docs: reorganize specification and model application protocol
This commit is contained in:
parent
f7ba1d539f
commit
16123747da
4 changed files with 1121 additions and 448 deletions
|
|
@ -1,5 +1,6 @@
|
|||
# Lazaret Configuration Template (config.toml)
|
||||
# Copy this file to ~/.config/lazaret/config.toml or ./lazaret.toml
|
||||
# Copy this file to ~/.config/lazaret/config.toml.
|
||||
# Lazaret configuration is host-only and must not be placed in a mounted workspace.
|
||||
|
||||
[storage]
|
||||
# Mount path on the host where persistent profile directories
|
||||
|
|
@ -8,7 +9,7 @@ data_root = "~/.local/share/lazaret"
|
|||
|
||||
[network]
|
||||
# Default global whitelist applied to the Netlink router gateway.
|
||||
# Nameservers from the host's /etc/resolv.conf are also appended dynamically.
|
||||
# Nameservers from the host's /etc/resolv.conf are added during Domain startup.
|
||||
default_allow = [
|
||||
"198.51.100.53/32", # Documentation address
|
||||
"192.0.2.53/32" # Documentation address
|
||||
|
|
@ -26,14 +27,14 @@ default_deny = [
|
|||
# Set to true to enable the agent-to-host escalation Unix Domain Socket server on the host.
|
||||
enabled = true
|
||||
|
||||
# Path on the host filesystem where profile-specific Unix escalation sockets are created.
|
||||
socket_dir = "/tmp/lazaret"
|
||||
# socket_dir is optional. Lazaret defaults to a private directory beneath
|
||||
# $XDG_RUNTIME_DIR.
|
||||
|
||||
# Host commands that the agent is allowed to execute on the host automatically
|
||||
# without triggering an interactive Human-In-The-Loop (HITL) prompt.
|
||||
auto_approve = [
|
||||
"aws sts get-caller-identity "
|
||||
]
|
||||
[[escalation.auto_approve]]
|
||||
argv = ["aws", "sts", "get-caller-identity"]
|
||||
allow_extra_args = false
|
||||
|
||||
# AI AGENT PROFILES
|
||||
|
||||
|
|
@ -77,11 +78,7 @@ volumes = [
|
|||
image = "docker.io/library/debian:trixie"
|
||||
persistent_dirs = ["/root", "/usr", "/var", "/etc", "/opt"]
|
||||
allow_egress = ["10.222.0.0/16"]
|
||||
env = {
|
||||
ANTHROPIC_API_KEY = "env:ANTHROPIC_API_KEY",
|
||||
OPENAI_API_KEY = "env:OPENAI_API_KEY",
|
||||
GEMINI_API_KEY = "env:GEMINI_API_KEY"
|
||||
}
|
||||
env = { ANTHROPIC_API_KEY = "env:ANTHROPIC_API_KEY", OPENAI_API_KEY = "env:OPENAI_API_KEY", GEMINI_API_KEY = "env:GEMINI_API_KEY" }
|
||||
volumes = [
|
||||
{ host = "{profile_dir}/opencode-config", container = "/root/.config/opencode", mode = "z" },
|
||||
{ host = "{profile_dir}/opencode-share", container = "/root/.local/share/opencode", mode = "z" }
|
||||
|
|
|
|||
53
docs/protocol/Lazaret.cfg
Normal file
53
docs/protocol/Lazaret.cfg
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
CONSTANTS
|
||||
D1 = d1
|
||||
S1 = s1
|
||||
S2 = s2
|
||||
R1 = r1
|
||||
R2 = r2
|
||||
R3 = r3
|
||||
ExactCmd = exactCmd
|
||||
PromptCmd = promptCmd
|
||||
ShellCmd = shellCmd
|
||||
EditedCmd = editedCmd
|
||||
Domains <- DomainsFixture
|
||||
Sessions <- SessionsFixture
|
||||
Requests <- RequestsFixture
|
||||
Commands <- CommandsFixture
|
||||
NoSession = NoSession
|
||||
NoRequest = NoRequest
|
||||
NoCommand = NoCommand
|
||||
NoApproval = NoApproval
|
||||
SessionDomain <- SessionDomainFixture
|
||||
SessionFingerprint <- SessionFingerprintFixture
|
||||
DomainFingerprint <- DomainFingerprintFixture
|
||||
ConfigSafe <- ConfigSafeFixture
|
||||
RequestSession <- RequestSessionFixture
|
||||
RequestCommand <- RequestCommandFixture
|
||||
CertificateValid <- CertificateValidFixture
|
||||
SignatureValid <- SignatureValidFixture
|
||||
AutoRuleMatches <- AutoRuleMatchesFixture
|
||||
CommandRequiresShell <- CommandRequiresShellFixture
|
||||
|
||||
SPECIFICATION Spec
|
||||
\* Fully closed or failed application states intentionally have no successor.
|
||||
CHECK_DEADLOCK FALSE
|
||||
|
||||
INVARIANTS
|
||||
TypeOK
|
||||
ConfigurationConfinement
|
||||
OrderedNetworkStartup
|
||||
PublishedDomainsReady
|
||||
RegisteredSessionsConnected
|
||||
AttachedSessionsValid
|
||||
AuthenticatedRequestsOnly
|
||||
HostExecutionAuthorized
|
||||
AutoApprovalNoShell
|
||||
TerminalIsolation
|
||||
NoOrphanHostProcess
|
||||
StateAnchorOwnership
|
||||
|
||||
PROPERTIES
|
||||
SessionEventuallyResolves
|
||||
RequestEventuallyResolves
|
||||
PromptEventuallyRestoresTerminal
|
||||
EmptyDomainEventuallyStops
|
||||
805
docs/protocol/Lazaret.tla
Normal file
805
docs/protocol/Lazaret.tla
Normal file
|
|
@ -0,0 +1,805 @@
|
|||
------------------------------ MODULE Lazaret ------------------------------
|
||||
EXTENDS Naturals, FiniteSets, Sequences, TLC
|
||||
|
||||
(***************************************************************************
|
||||
Application-wide protocol model for Lazaret.
|
||||
|
||||
The model covers Domain startup, ordered resource preparation, Client Session
|
||||
registration, host escalation, terminal ownership, failure, and teardown.
|
||||
Cryptographic algorithms, serialization, container commands, filesystems, and
|
||||
network packet handling are refinement boundaries validated by implementation
|
||||
tests rather than modeled internally.
|
||||
***************************************************************************)
|
||||
|
||||
CONSTANTS
|
||||
Domains,
|
||||
Sessions,
|
||||
Requests,
|
||||
Commands,
|
||||
NoSession,
|
||||
NoRequest,
|
||||
NoCommand,
|
||||
NoApproval,
|
||||
SessionDomain,
|
||||
SessionFingerprint,
|
||||
DomainFingerprint,
|
||||
ConfigSafe,
|
||||
RequestSession,
|
||||
RequestCommand,
|
||||
CertificateValid,
|
||||
SignatureValid,
|
||||
AutoRuleMatches,
|
||||
CommandRequiresShell,
|
||||
D1,
|
||||
S1,
|
||||
S2,
|
||||
R1,
|
||||
R2,
|
||||
R3,
|
||||
ExactCmd,
|
||||
PromptCmd,
|
||||
ShellCmd,
|
||||
EditedCmd
|
||||
|
||||
ASSUME
|
||||
/\ IsFiniteSet(Domains)
|
||||
/\ IsFiniteSet(Sessions)
|
||||
/\ IsFiniteSet(Requests)
|
||||
/\ IsFiniteSet(Commands)
|
||||
/\ Domains # {}
|
||||
/\ Sessions # {}
|
||||
/\ Requests # {}
|
||||
/\ NoSession \notin Sessions
|
||||
/\ NoRequest \notin Requests
|
||||
/\ NoCommand \notin Commands
|
||||
/\ SessionDomain \in [Sessions -> Domains]
|
||||
/\ SessionFingerprint \in [Sessions -> STRING]
|
||||
/\ DomainFingerprint \in [Domains -> STRING]
|
||||
/\ ConfigSafe \in [Domains -> BOOLEAN]
|
||||
/\ RequestSession \in [Requests -> Sessions]
|
||||
/\ RequestCommand \in [Requests -> Commands]
|
||||
/\ CertificateValid \in [Requests -> BOOLEAN]
|
||||
/\ SignatureValid \in [Requests -> BOOLEAN]
|
||||
/\ AutoRuleMatches \in [Requests -> BOOLEAN]
|
||||
/\ CommandRequiresShell \in [Commands -> BOOLEAN]
|
||||
|
||||
DomainPhases == {"Absent", "Starting", "Ready", "Stopping", "Failed"}
|
||||
CoordinatorStates == {"Absent", "Starting", "Ready", "Stopping", "Failed"}
|
||||
MountStates == {"Unmounted", "Mounted"}
|
||||
ResourceStates == {"Stopped", "Ready", "Failed"}
|
||||
SessionPhases == {
|
||||
"New", "Waiting", "Registering", "Attached", "Prompting",
|
||||
"HostExecuting", "Closed", "Failed"
|
||||
}
|
||||
TerminalRoutes == {"Detached", "Sandbox", "Prompt", "HostProcess"}
|
||||
ConnectionStates == {"Disconnected", "Connected"}
|
||||
RequestPhases == {
|
||||
"Unused", "Verified", "AwaitingApproval", "Approved", "Executing",
|
||||
"Completed", "Denied", "TimedOut", "Cancelled", "Failed"
|
||||
}
|
||||
ApprovalSources == {NoApproval, "AutoRule", "Developer"}
|
||||
TerminalRequestPhases == {"Completed", "Denied", "TimedOut", "Cancelled", "Failed"}
|
||||
|
||||
(***************************************************************************
|
||||
Finite fixture operators used by Lazaret.cfg. The protocol itself depends only
|
||||
on the abstract constants above; other TLC configurations may override them with
|
||||
different finite operators.
|
||||
***************************************************************************)
|
||||
|
||||
DomainsFixture == {D1}
|
||||
SessionsFixture == {S1, S2}
|
||||
RequestsFixture == {R1, R2, R3}
|
||||
CommandsFixture == {ExactCmd, PromptCmd, ShellCmd, EditedCmd}
|
||||
|
||||
SessionDomainFixture == (S1 :> D1) @@ (S2 :> D1)
|
||||
SessionFingerprintFixture ==
|
||||
(S1 :> "config-a") @@ (S2 :> "config-a")
|
||||
DomainFingerprintFixture == D1 :> "config-a"
|
||||
ConfigSafeFixture == D1 :> TRUE
|
||||
RequestSessionFixture ==
|
||||
(R1 :> S1) @@ (R2 :> S2) @@ (R3 :> S1)
|
||||
RequestCommandFixture ==
|
||||
(R1 :> ExactCmd) @@ (R2 :> PromptCmd) @@ (R3 :> ShellCmd)
|
||||
CertificateValidFixture ==
|
||||
(R1 :> TRUE) @@ (R2 :> TRUE) @@ (R3 :> FALSE)
|
||||
SignatureValidFixture ==
|
||||
(R1 :> TRUE) @@ (R2 :> TRUE) @@ (R3 :> FALSE)
|
||||
AutoRuleMatchesFixture ==
|
||||
(R1 :> TRUE) @@ (R2 :> FALSE) @@ (R3 :> TRUE)
|
||||
CommandRequiresShellFixture ==
|
||||
(ExactCmd :> FALSE) @@
|
||||
(PromptCmd :> FALSE) @@
|
||||
(ShellCmd :> TRUE) @@
|
||||
(EditedCmd :> FALSE)
|
||||
|
||||
VARIABLES
|
||||
domainPhase,
|
||||
coordinatorState,
|
||||
stateAnchorState,
|
||||
sidecarState,
|
||||
sandboxState,
|
||||
trustReady,
|
||||
lockOwner,
|
||||
published,
|
||||
registered,
|
||||
connected,
|
||||
sessionPhase,
|
||||
terminalRoute,
|
||||
connectionState,
|
||||
activeRequest,
|
||||
requestPhase,
|
||||
approvalSource,
|
||||
approvedCommand,
|
||||
hostRunning
|
||||
|
||||
vars == <<
|
||||
domainPhase,
|
||||
coordinatorState,
|
||||
stateAnchorState,
|
||||
sidecarState,
|
||||
sandboxState,
|
||||
trustReady,
|
||||
lockOwner,
|
||||
published,
|
||||
registered,
|
||||
connected,
|
||||
sessionPhase,
|
||||
terminalRoute,
|
||||
connectionState,
|
||||
activeRequest,
|
||||
requestPhase,
|
||||
approvalSource,
|
||||
approvedCommand,
|
||||
hostRunning
|
||||
>>
|
||||
|
||||
Dom(s) == SessionDomain[s]
|
||||
ReqSession(r) == RequestSession[r]
|
||||
ReqDomain(r) == Dom(ReqSession(r))
|
||||
IsTerminalRequest(r) == requestPhase[r] \in TerminalRequestPhases
|
||||
|
||||
Interested(d) == {
|
||||
s \in Sessions :
|
||||
Dom(s) = d /\
|
||||
sessionPhase[s] \in {
|
||||
"Waiting", "Registering", "Attached", "Prompting", "HostExecuting"
|
||||
}
|
||||
}
|
||||
|
||||
RequestsForDomain(d) == {r \in Requests : ReqDomain(r) = d}
|
||||
|
||||
Init ==
|
||||
/\ domainPhase = [d \in Domains |-> "Absent"]
|
||||
/\ coordinatorState = [d \in Domains |-> "Absent"]
|
||||
/\ stateAnchorState = [d \in Domains |-> "Unmounted"]
|
||||
/\ sidecarState = [d \in Domains |-> "Stopped"]
|
||||
/\ sandboxState = [d \in Domains |-> "Stopped"]
|
||||
/\ trustReady = [d \in Domains |-> FALSE]
|
||||
/\ lockOwner = [d \in Domains |-> NoSession]
|
||||
/\ published = {}
|
||||
/\ registered = [d \in Domains |-> {}]
|
||||
/\ connected = [d \in Domains |-> {}]
|
||||
/\ sessionPhase = [s \in Sessions |-> "New"]
|
||||
/\ terminalRoute = [s \in Sessions |-> "Detached"]
|
||||
/\ connectionState = [s \in Sessions |-> "Disconnected"]
|
||||
/\ activeRequest = [s \in Sessions |-> NoRequest]
|
||||
/\ requestPhase = [r \in Requests |-> "Unused"]
|
||||
/\ approvalSource = [r \in Requests |-> NoApproval]
|
||||
/\ approvedCommand = [r \in Requests |-> NoCommand]
|
||||
/\ hostRunning = {}
|
||||
|
||||
(***************************************************************************
|
||||
Domain and Session lifecycle
|
||||
***************************************************************************)
|
||||
|
||||
StartNewDomain(s) ==
|
||||
LET d == Dom(s) IN
|
||||
/\ sessionPhase[s] = "New"
|
||||
/\ ConfigSafe[d]
|
||||
/\ domainPhase[d] = "Absent"
|
||||
/\ domainPhase' = [domainPhase EXCEPT ![d] = "Starting"]
|
||||
/\ coordinatorState' = [coordinatorState EXCEPT ![d] = "Starting"]
|
||||
/\ lockOwner' = [lockOwner EXCEPT ![d] = s]
|
||||
/\ sessionPhase' = [sessionPhase EXCEPT ![s] = "Waiting"]
|
||||
/\ UNCHANGED <<
|
||||
stateAnchorState, sidecarState, sandboxState, trustReady, published,
|
||||
registered, connected, terminalRoute, connectionState, activeRequest,
|
||||
requestPhase, approvalSource, approvedCommand, hostRunning
|
||||
>>
|
||||
|
||||
StartReadySession(s) ==
|
||||
LET d == Dom(s) IN
|
||||
/\ sessionPhase[s] = "New"
|
||||
/\ domainPhase[d] = "Ready"
|
||||
/\ SessionFingerprint[s] = DomainFingerprint[d]
|
||||
/\ sessionPhase' = [sessionPhase EXCEPT ![s] = "Registering"]
|
||||
/\ connectionState' = [connectionState EXCEPT ![s] = "Connected"]
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, stateAnchorState, sidecarState,
|
||||
sandboxState, trustReady, lockOwner, published, registered, connected,
|
||||
terminalRoute, activeRequest, requestPhase, approvalSource,
|
||||
approvedCommand, hostRunning
|
||||
>>
|
||||
|
||||
WaitForDomain(s) ==
|
||||
LET d == Dom(s) IN
|
||||
/\ sessionPhase[s] = "New"
|
||||
/\ domainPhase[d] \in {"Starting", "Stopping"}
|
||||
/\ sessionPhase' = [sessionPhase EXCEPT ![s] = "Waiting"]
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, stateAnchorState, sidecarState,
|
||||
sandboxState, trustReady, lockOwner, published, registered, connected,
|
||||
terminalRoute, connectionState, activeRequest, requestPhase,
|
||||
approvalSource, approvedCommand, hostRunning
|
||||
>>
|
||||
|
||||
RestartWaitingDomain(s) ==
|
||||
LET d == Dom(s) IN
|
||||
/\ sessionPhase[s] = "Waiting"
|
||||
/\ ConfigSafe[d]
|
||||
/\ domainPhase[d] = "Absent"
|
||||
/\ domainPhase' = [domainPhase EXCEPT ![d] = "Starting"]
|
||||
/\ coordinatorState' = [coordinatorState EXCEPT ![d] = "Starting"]
|
||||
/\ lockOwner' = [lockOwner EXCEPT ![d] = s]
|
||||
/\ UNCHANGED <<
|
||||
stateAnchorState, sidecarState, sandboxState, trustReady, published,
|
||||
registered, connected, sessionPhase, terminalRoute, connectionState,
|
||||
activeRequest, requestPhase, approvalSource, approvedCommand,
|
||||
hostRunning
|
||||
>>
|
||||
|
||||
MountStateAnchor(d) ==
|
||||
/\ domainPhase[d] = "Starting"
|
||||
/\ coordinatorState[d] = "Starting"
|
||||
/\ stateAnchorState[d] = "Unmounted"
|
||||
/\ stateAnchorState' = [stateAnchorState EXCEPT ![d] = "Mounted"]
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, sidecarState, sandboxState, trustReady,
|
||||
lockOwner, published, registered, connected, sessionPhase,
|
||||
terminalRoute, connectionState, activeRequest, requestPhase,
|
||||
approvalSource, approvedCommand, hostRunning
|
||||
>>
|
||||
|
||||
CreateTrustAnchor(d) ==
|
||||
/\ domainPhase[d] = "Starting"
|
||||
/\ coordinatorState[d] = "Starting"
|
||||
/\ ~trustReady[d]
|
||||
/\ trustReady' = [trustReady EXCEPT ![d] = TRUE]
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, stateAnchorState, sidecarState,
|
||||
sandboxState, lockOwner, published, registered, connected,
|
||||
sessionPhase, terminalRoute, connectionState, activeRequest,
|
||||
requestPhase, approvalSource, approvedCommand, hostRunning
|
||||
>>
|
||||
|
||||
StartSidecar(d) ==
|
||||
/\ domainPhase[d] = "Starting"
|
||||
/\ stateAnchorState[d] = "Mounted"
|
||||
/\ trustReady[d]
|
||||
/\ sidecarState[d] = "Stopped"
|
||||
/\ sidecarState' = [sidecarState EXCEPT ![d] = "Ready"]
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, stateAnchorState, sandboxState,
|
||||
trustReady, lockOwner, published, registered, connected, sessionPhase,
|
||||
terminalRoute, connectionState, activeRequest, requestPhase,
|
||||
approvalSource, approvedCommand, hostRunning
|
||||
>>
|
||||
|
||||
StartSandbox(d) ==
|
||||
/\ domainPhase[d] = "Starting"
|
||||
/\ sidecarState[d] = "Ready"
|
||||
/\ sandboxState[d] = "Stopped"
|
||||
/\ sandboxState' = [sandboxState EXCEPT ![d] = "Ready"]
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, stateAnchorState, sidecarState,
|
||||
trustReady, lockOwner, published, registered, connected, sessionPhase,
|
||||
terminalRoute, connectionState, activeRequest, requestPhase,
|
||||
approvalSource, approvedCommand, hostRunning
|
||||
>>
|
||||
|
||||
PublishDomain(d) ==
|
||||
/\ domainPhase[d] = "Starting"
|
||||
/\ coordinatorState[d] = "Starting"
|
||||
/\ stateAnchorState[d] = "Mounted"
|
||||
/\ sidecarState[d] = "Ready"
|
||||
/\ sandboxState[d] = "Ready"
|
||||
/\ trustReady[d]
|
||||
/\ domainPhase' = [domainPhase EXCEPT ![d] = "Ready"]
|
||||
/\ coordinatorState' = [coordinatorState EXCEPT ![d] = "Ready"]
|
||||
/\ lockOwner' = [lockOwner EXCEPT ![d] = NoSession]
|
||||
/\ published' = published \cup {d}
|
||||
/\ UNCHANGED <<
|
||||
stateAnchorState, sidecarState, sandboxState, trustReady, registered,
|
||||
connected, sessionPhase, terminalRoute, connectionState,
|
||||
activeRequest, requestPhase, approvalSource, approvedCommand,
|
||||
hostRunning
|
||||
>>
|
||||
|
||||
AdvanceWaitingSession(s) ==
|
||||
LET d == Dom(s) IN
|
||||
/\ sessionPhase[s] = "Waiting"
|
||||
/\ domainPhase[d] = "Ready"
|
||||
/\ SessionFingerprint[s] = DomainFingerprint[d]
|
||||
/\ sessionPhase' = [sessionPhase EXCEPT ![s] = "Registering"]
|
||||
/\ connectionState' = [connectionState EXCEPT ![s] = "Connected"]
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, stateAnchorState, sidecarState,
|
||||
sandboxState, trustReady, lockOwner, published, registered, connected,
|
||||
terminalRoute, activeRequest, requestPhase, approvalSource,
|
||||
approvedCommand, hostRunning
|
||||
>>
|
||||
|
||||
RejectConfigurationMismatch(s) ==
|
||||
LET d == Dom(s) IN
|
||||
/\ sessionPhase[s] \in {"New", "Waiting"}
|
||||
/\ domainPhase[d] = "Ready"
|
||||
/\ SessionFingerprint[s] # DomainFingerprint[d]
|
||||
/\ sessionPhase' = [sessionPhase EXCEPT ![s] = "Failed"]
|
||||
/\ terminalRoute' = [terminalRoute EXCEPT ![s] = "Detached"]
|
||||
/\ connectionState' = [connectionState EXCEPT ![s] = "Disconnected"]
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, stateAnchorState, sidecarState,
|
||||
sandboxState, trustReady, lockOwner, published, registered, connected,
|
||||
activeRequest, requestPhase, approvalSource, approvedCommand,
|
||||
hostRunning
|
||||
>>
|
||||
|
||||
RegisterSession(s) ==
|
||||
LET d == Dom(s) IN
|
||||
/\ sessionPhase[s] = "Registering"
|
||||
/\ connectionState[s] = "Connected"
|
||||
/\ domainPhase[d] = "Ready"
|
||||
/\ SessionFingerprint[s] = DomainFingerprint[d]
|
||||
/\ registered' = [registered EXCEPT ![d] = @ \cup {s}]
|
||||
/\ connected' = [connected EXCEPT ![d] = @ \cup {s}]
|
||||
/\ sessionPhase' = [sessionPhase EXCEPT ![s] = "Attached"]
|
||||
/\ terminalRoute' = [terminalRoute EXCEPT ![s] = "Sandbox"]
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, stateAnchorState, sidecarState,
|
||||
sandboxState, trustReady, lockOwner, published, connectionState,
|
||||
activeRequest, requestPhase, approvalSource, approvedCommand,
|
||||
hostRunning
|
||||
>>
|
||||
|
||||
DisconnectSession(s) ==
|
||||
LET d == Dom(s)
|
||||
r == activeRequest[s]
|
||||
IN
|
||||
/\ sessionPhase[s] \in {
|
||||
"Waiting", "Registering", "Attached", "Prompting", "HostExecuting"
|
||||
}
|
||||
/\ registered' = [registered EXCEPT ![d] = @ \ {s}]
|
||||
/\ connected' = [connected EXCEPT ![d] = @ \ {s}]
|
||||
/\ sessionPhase' = [sessionPhase EXCEPT ![s] = "Closed"]
|
||||
/\ terminalRoute' = [terminalRoute EXCEPT ![s] = "Detached"]
|
||||
/\ connectionState' = [connectionState EXCEPT ![s] = "Disconnected"]
|
||||
/\ activeRequest' = [activeRequest EXCEPT ![s] = NoRequest]
|
||||
/\ requestPhase' = [q \in Requests |->
|
||||
IF r # NoRequest /\ q = r /\ ~IsTerminalRequest(q)
|
||||
THEN "Cancelled"
|
||||
ELSE requestPhase[q]]
|
||||
/\ hostRunning' = IF r = NoRequest THEN hostRunning ELSE hostRunning \ {r}
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, stateAnchorState, sidecarState,
|
||||
sandboxState, trustReady, lockOwner, published, approvalSource,
|
||||
approvedCommand
|
||||
>>
|
||||
|
||||
BeginTeardown(d) ==
|
||||
/\ domainPhase[d] = "Ready"
|
||||
/\ Interested(d) = {}
|
||||
/\ registered[d] = {}
|
||||
/\ connected[d] = {}
|
||||
/\ domainPhase' = [domainPhase EXCEPT ![d] = "Stopping"]
|
||||
/\ coordinatorState' = [coordinatorState EXCEPT ![d] = "Stopping"]
|
||||
/\ published' = published \ {d}
|
||||
/\ UNCHANGED <<
|
||||
stateAnchorState, sidecarState, sandboxState, trustReady, lockOwner,
|
||||
registered, connected, sessionPhase, terminalRoute, connectionState,
|
||||
activeRequest, requestPhase, approvalSource, approvedCommand,
|
||||
hostRunning
|
||||
>>
|
||||
|
||||
StopSandbox(d) ==
|
||||
/\ domainPhase[d] = "Stopping"
|
||||
/\ sandboxState[d] = "Ready"
|
||||
/\ sandboxState' = [sandboxState EXCEPT ![d] = "Stopped"]
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, stateAnchorState, sidecarState,
|
||||
trustReady, lockOwner, published, registered, connected, sessionPhase,
|
||||
terminalRoute, connectionState, activeRequest, requestPhase,
|
||||
approvalSource, approvedCommand, hostRunning
|
||||
>>
|
||||
|
||||
StopSidecar(d) ==
|
||||
/\ domainPhase[d] = "Stopping"
|
||||
/\ sandboxState[d] = "Stopped"
|
||||
/\ sidecarState[d] = "Ready"
|
||||
/\ sidecarState' = [sidecarState EXCEPT ![d] = "Stopped"]
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, stateAnchorState, sandboxState,
|
||||
trustReady, lockOwner, published, registered, connected, sessionPhase,
|
||||
terminalRoute, connectionState, activeRequest, requestPhase,
|
||||
approvalSource, approvedCommand, hostRunning
|
||||
>>
|
||||
|
||||
UnmountStateAnchor(d) ==
|
||||
/\ domainPhase[d] = "Stopping"
|
||||
/\ sandboxState[d] = "Stopped"
|
||||
/\ sidecarState[d] = "Stopped"
|
||||
/\ stateAnchorState[d] = "Mounted"
|
||||
/\ stateAnchorState' = [stateAnchorState EXCEPT ![d] = "Unmounted"]
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, sidecarState, sandboxState, trustReady,
|
||||
lockOwner, published, registered, connected, sessionPhase,
|
||||
terminalRoute, connectionState, activeRequest, requestPhase,
|
||||
approvalSource, approvedCommand, hostRunning
|
||||
>>
|
||||
|
||||
FinishTeardown(d) ==
|
||||
/\ domainPhase[d] = "Stopping"
|
||||
/\ stateAnchorState[d] = "Unmounted"
|
||||
/\ sidecarState[d] = "Stopped"
|
||||
/\ sandboxState[d] = "Stopped"
|
||||
/\ domainPhase' = [domainPhase EXCEPT ![d] = "Absent"]
|
||||
/\ coordinatorState' = [coordinatorState EXCEPT ![d] = "Absent"]
|
||||
/\ trustReady' = [trustReady EXCEPT ![d] = FALSE]
|
||||
/\ lockOwner' = [lockOwner EXCEPT ![d] = NoSession]
|
||||
/\ UNCHANGED <<
|
||||
stateAnchorState, sidecarState, sandboxState, published, registered,
|
||||
connected, sessionPhase, terminalRoute, connectionState,
|
||||
activeRequest, requestPhase, approvalSource, approvedCommand,
|
||||
hostRunning
|
||||
>>
|
||||
|
||||
(***************************************************************************
|
||||
Host escalation protocol
|
||||
***************************************************************************)
|
||||
|
||||
ReceiveEscalation(r) ==
|
||||
LET s == ReqSession(r)
|
||||
d == ReqDomain(r)
|
||||
IN
|
||||
/\ requestPhase[r] = "Unused"
|
||||
/\ sessionPhase[s] = "Attached"
|
||||
/\ activeRequest[s] = NoRequest
|
||||
/\ s \in registered[d]
|
||||
/\ s \in connected[d]
|
||||
/\ CertificateValid[r]
|
||||
/\ SignatureValid[r]
|
||||
/\ requestPhase' = [requestPhase EXCEPT ![r] = "Verified"]
|
||||
/\ activeRequest' = [activeRequest EXCEPT ![s] = r]
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, stateAnchorState, sidecarState,
|
||||
sandboxState, trustReady, lockOwner, published, registered, connected,
|
||||
sessionPhase, terminalRoute, connectionState, approvalSource,
|
||||
approvedCommand, hostRunning
|
||||
>>
|
||||
|
||||
AutoApprove(r) ==
|
||||
/\ requestPhase[r] = "Verified"
|
||||
/\ AutoRuleMatches[r]
|
||||
/\ ~CommandRequiresShell[RequestCommand[r]]
|
||||
/\ requestPhase' = [requestPhase EXCEPT ![r] = "Approved"]
|
||||
/\ approvalSource' = [approvalSource EXCEPT ![r] = "AutoRule"]
|
||||
/\ approvedCommand' = [approvedCommand EXCEPT ![r] = RequestCommand[r]]
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, stateAnchorState, sidecarState,
|
||||
sandboxState, trustReady, lockOwner, published, registered, connected,
|
||||
sessionPhase, terminalRoute, connectionState, activeRequest, hostRunning
|
||||
>>
|
||||
|
||||
QueuePrompt(r) ==
|
||||
/\ requestPhase[r] = "Verified"
|
||||
/\ ~(AutoRuleMatches[r] /\ ~CommandRequiresShell[RequestCommand[r]])
|
||||
/\ requestPhase' = [requestPhase EXCEPT ![r] = "AwaitingApproval"]
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, stateAnchorState, sidecarState,
|
||||
sandboxState, trustReady, lockOwner, published, registered, connected,
|
||||
sessionPhase, terminalRoute, connectionState, activeRequest,
|
||||
approvalSource, approvedCommand, hostRunning
|
||||
>>
|
||||
|
||||
BeginPrompt(r) ==
|
||||
LET s == ReqSession(r) IN
|
||||
/\ requestPhase[r] = "AwaitingApproval"
|
||||
/\ sessionPhase[s] = "Attached"
|
||||
/\ activeRequest[s] = r
|
||||
/\ sessionPhase' = [sessionPhase EXCEPT ![s] = "Prompting"]
|
||||
/\ terminalRoute' = [terminalRoute EXCEPT ![s] = "Prompt"]
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, stateAnchorState, sidecarState,
|
||||
sandboxState, trustReady, lockOwner, published, registered, connected,
|
||||
connectionState, activeRequest, requestPhase, approvalSource,
|
||||
approvedCommand, hostRunning
|
||||
>>
|
||||
|
||||
DeveloperApprove(r, cmd) ==
|
||||
LET s == ReqSession(r) IN
|
||||
/\ requestPhase[r] = "AwaitingApproval"
|
||||
/\ sessionPhase[s] = "Prompting"
|
||||
/\ cmd \in Commands
|
||||
/\ requestPhase' = [requestPhase EXCEPT ![r] = "Approved"]
|
||||
/\ approvalSource' = [approvalSource EXCEPT ![r] = "Developer"]
|
||||
/\ approvedCommand' = [approvedCommand EXCEPT ![r] = cmd]
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, stateAnchorState, sidecarState,
|
||||
sandboxState, trustReady, lockOwner, published, registered, connected,
|
||||
sessionPhase, terminalRoute, connectionState, activeRequest, hostRunning
|
||||
>>
|
||||
|
||||
DeveloperDeny(r) ==
|
||||
LET s == ReqSession(r) IN
|
||||
/\ requestPhase[r] = "AwaitingApproval"
|
||||
/\ sessionPhase[s] = "Prompting"
|
||||
/\ requestPhase' = [requestPhase EXCEPT ![r] = "Denied"]
|
||||
/\ sessionPhase' = [sessionPhase EXCEPT ![s] = "Attached"]
|
||||
/\ terminalRoute' = [terminalRoute EXCEPT ![s] = "Sandbox"]
|
||||
/\ activeRequest' = [activeRequest EXCEPT ![s] = NoRequest]
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, stateAnchorState, sidecarState,
|
||||
sandboxState, trustReady, lockOwner, published, registered, connected,
|
||||
connectionState, approvalSource, approvedCommand, hostRunning
|
||||
>>
|
||||
|
||||
StartHostExecution(r) ==
|
||||
LET s == ReqSession(r)
|
||||
d == ReqDomain(r)
|
||||
IN
|
||||
/\ requestPhase[r] = "Approved"
|
||||
/\ activeRequest[s] = r
|
||||
/\ s \in connected[d]
|
||||
/\ sessionPhase[s] \in {"Attached", "Prompting"}
|
||||
/\ requestPhase' = [requestPhase EXCEPT ![r] = "Executing"]
|
||||
/\ sessionPhase' = [sessionPhase EXCEPT ![s] = "HostExecuting"]
|
||||
/\ terminalRoute' = [terminalRoute EXCEPT ![s] = "HostProcess"]
|
||||
/\ hostRunning' = hostRunning \cup {r}
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, stateAnchorState, sidecarState,
|
||||
sandboxState, trustReady, lockOwner, published, registered, connected,
|
||||
connectionState, activeRequest, approvalSource, approvedCommand
|
||||
>>
|
||||
|
||||
CompleteHostExecution(r) ==
|
||||
LET s == ReqSession(r) IN
|
||||
/\ requestPhase[r] = "Executing"
|
||||
/\ r \in hostRunning
|
||||
/\ requestPhase' = [requestPhase EXCEPT ![r] = "Completed"]
|
||||
/\ hostRunning' = hostRunning \ {r}
|
||||
/\ sessionPhase' = [sessionPhase EXCEPT ![s] = "Attached"]
|
||||
/\ terminalRoute' = [terminalRoute EXCEPT ![s] = "Sandbox"]
|
||||
/\ activeRequest' = [activeRequest EXCEPT ![s] = NoRequest]
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, stateAnchorState, sidecarState,
|
||||
sandboxState, trustReady, lockOwner, published, registered, connected,
|
||||
connectionState, approvalSource, approvedCommand
|
||||
>>
|
||||
|
||||
TimeoutRequest(r) ==
|
||||
LET s == ReqSession(r) IN
|
||||
/\ requestPhase[r] \in {"Verified", "AwaitingApproval", "Approved"}
|
||||
/\ activeRequest[s] = r
|
||||
/\ requestPhase' = [requestPhase EXCEPT ![r] = "TimedOut"]
|
||||
/\ sessionPhase' = [sessionPhase EXCEPT ![s] = "Attached"]
|
||||
/\ terminalRoute' = [terminalRoute EXCEPT ![s] = "Sandbox"]
|
||||
/\ activeRequest' = [activeRequest EXCEPT ![s] = NoRequest]
|
||||
/\ UNCHANGED <<
|
||||
domainPhase, coordinatorState, stateAnchorState, sidecarState,
|
||||
sandboxState, trustReady, lockOwner, published, registered, connected,
|
||||
connectionState, approvalSource, approvedCommand, hostRunning
|
||||
>>
|
||||
|
||||
(***************************************************************************
|
||||
Failure and recovery
|
||||
***************************************************************************)
|
||||
|
||||
FailDomain(d) ==
|
||||
/\ domainPhase[d] \in {"Starting", "Ready", "Stopping"}
|
||||
/\ domainPhase' = [domainPhase EXCEPT ![d] = "Failed"]
|
||||
/\ coordinatorState' = [coordinatorState EXCEPT ![d] = "Failed"]
|
||||
/\ stateAnchorState' = [stateAnchorState EXCEPT ![d] = "Unmounted"]
|
||||
/\ sidecarState' = [sidecarState EXCEPT ![d] = "Stopped"]
|
||||
/\ sandboxState' = [sandboxState EXCEPT ![d] = "Stopped"]
|
||||
/\ trustReady' = [trustReady EXCEPT ![d] = FALSE]
|
||||
/\ lockOwner' = [lockOwner EXCEPT ![d] = NoSession]
|
||||
/\ published' = published \ {d}
|
||||
/\ registered' = [registered EXCEPT ![d] = {}]
|
||||
/\ connected' = [connected EXCEPT ![d] = {}]
|
||||
/\ sessionPhase' = [s \in Sessions |->
|
||||
IF Dom(s) = d /\ sessionPhase[s] \notin {"New", "Closed"}
|
||||
THEN "Failed"
|
||||
ELSE sessionPhase[s]]
|
||||
/\ terminalRoute' = [s \in Sessions |->
|
||||
IF Dom(s) = d THEN "Detached" ELSE terminalRoute[s]]
|
||||
/\ connectionState' = [s \in Sessions |->
|
||||
IF Dom(s) = d THEN "Disconnected" ELSE connectionState[s]]
|
||||
/\ activeRequest' = [s \in Sessions |->
|
||||
IF Dom(s) = d THEN NoRequest ELSE activeRequest[s]]
|
||||
/\ requestPhase' = [r \in Requests |->
|
||||
IF ReqDomain(r) = d /\ requestPhase[r] # "Unused" /\ ~IsTerminalRequest(r)
|
||||
THEN "Failed"
|
||||
ELSE requestPhase[r]]
|
||||
/\ hostRunning' = hostRunning \ RequestsForDomain(d)
|
||||
/\ UNCHANGED <<approvalSource, approvedCommand>>
|
||||
|
||||
ResetFailedDomain(d) ==
|
||||
/\ domainPhase[d] = "Failed"
|
||||
/\ domainPhase' = [domainPhase EXCEPT ![d] = "Absent"]
|
||||
/\ coordinatorState' = [coordinatorState EXCEPT ![d] = "Absent"]
|
||||
/\ UNCHANGED <<
|
||||
stateAnchorState, sidecarState, sandboxState, trustReady, lockOwner,
|
||||
published, registered, connected, sessionPhase, terminalRoute,
|
||||
connectionState, activeRequest, requestPhase, approvalSource,
|
||||
approvedCommand, hostRunning
|
||||
>>
|
||||
|
||||
DomainProgress(d) ==
|
||||
\/ MountStateAnchor(d)
|
||||
\/ CreateTrustAnchor(d)
|
||||
\/ StartSidecar(d)
|
||||
\/ StartSandbox(d)
|
||||
\/ PublishDomain(d)
|
||||
\/ BeginTeardown(d)
|
||||
\/ StopSandbox(d)
|
||||
\/ StopSidecar(d)
|
||||
\/ UnmountStateAnchor(d)
|
||||
\/ FinishTeardown(d)
|
||||
\/ ResetFailedDomain(d)
|
||||
|
||||
SessionProgress(s) ==
|
||||
\/ StartNewDomain(s)
|
||||
\/ StartReadySession(s)
|
||||
\/ WaitForDomain(s)
|
||||
\/ RestartWaitingDomain(s)
|
||||
\/ AdvanceWaitingSession(s)
|
||||
\/ RejectConfigurationMismatch(s)
|
||||
\/ RegisterSession(s)
|
||||
|
||||
RequestProgress(r) ==
|
||||
\/ AutoApprove(r)
|
||||
\/ QueuePrompt(r)
|
||||
\/ BeginPrompt(r)
|
||||
\/ (\E cmd \in Commands : DeveloperApprove(r, cmd))
|
||||
\/ DeveloperDeny(r)
|
||||
\/ StartHostExecution(r)
|
||||
\/ CompleteHostExecution(r)
|
||||
\/ TimeoutRequest(r)
|
||||
|
||||
Next ==
|
||||
\/ (\E s \in Sessions :
|
||||
StartNewDomain(s) \/ StartReadySession(s) \/ WaitForDomain(s) \/
|
||||
RestartWaitingDomain(s) \/ AdvanceWaitingSession(s) \/
|
||||
RejectConfigurationMismatch(s) \/
|
||||
RegisterSession(s) \/ DisconnectSession(s))
|
||||
\/ (\E d \in Domains :
|
||||
DomainProgress(d) \/ FailDomain(d))
|
||||
\/ (\E r \in Requests :
|
||||
ReceiveEscalation(r) \/ RequestProgress(r))
|
||||
|
||||
Spec ==
|
||||
/\ Init
|
||||
/\ [][Next]_vars
|
||||
/\ \A d \in Domains : WF_vars(DomainProgress(d))
|
||||
/\ \A s \in Sessions : WF_vars(SessionProgress(s))
|
||||
/\ \A r \in Requests : WF_vars(RequestProgress(r))
|
||||
|
||||
(***************************************************************************
|
||||
Safety properties
|
||||
***************************************************************************)
|
||||
|
||||
TypeOK ==
|
||||
/\ domainPhase \in [Domains -> DomainPhases]
|
||||
/\ coordinatorState \in [Domains -> CoordinatorStates]
|
||||
/\ stateAnchorState \in [Domains -> MountStates]
|
||||
/\ sidecarState \in [Domains -> ResourceStates]
|
||||
/\ sandboxState \in [Domains -> ResourceStates]
|
||||
/\ trustReady \in [Domains -> BOOLEAN]
|
||||
/\ lockOwner \in [Domains -> Sessions \cup {NoSession}]
|
||||
/\ published \subseteq Domains
|
||||
/\ registered \in [Domains -> SUBSET Sessions]
|
||||
/\ connected \in [Domains -> SUBSET Sessions]
|
||||
/\ sessionPhase \in [Sessions -> SessionPhases]
|
||||
/\ terminalRoute \in [Sessions -> TerminalRoutes]
|
||||
/\ connectionState \in [Sessions -> ConnectionStates]
|
||||
/\ activeRequest \in [Sessions -> Requests \cup {NoRequest}]
|
||||
/\ requestPhase \in [Requests -> RequestPhases]
|
||||
/\ approvalSource \in [Requests -> ApprovalSources]
|
||||
/\ approvedCommand \in [Requests -> Commands \cup {NoCommand}]
|
||||
/\ hostRunning \subseteq Requests
|
||||
|
||||
ConfigurationConfinement ==
|
||||
\A d \in Domains : domainPhase[d] # "Absent" => ConfigSafe[d]
|
||||
|
||||
OrderedNetworkStartup ==
|
||||
\A d \in Domains : sandboxState[d] = "Ready" => sidecarState[d] = "Ready"
|
||||
|
||||
PublishedDomainsReady ==
|
||||
\A d \in published :
|
||||
/\ domainPhase[d] = "Ready"
|
||||
/\ coordinatorState[d] = "Ready"
|
||||
/\ stateAnchorState[d] = "Mounted"
|
||||
/\ sidecarState[d] = "Ready"
|
||||
/\ sandboxState[d] = "Ready"
|
||||
/\ trustReady[d]
|
||||
|
||||
RegisteredSessionsConnected ==
|
||||
\A d \in Domains : registered[d] \subseteq connected[d]
|
||||
|
||||
AttachedSessionsValid ==
|
||||
\A s \in Sessions :
|
||||
sessionPhase[s] \in {"Attached", "Prompting", "HostExecuting"} =>
|
||||
/\ s \in registered[Dom(s)]
|
||||
/\ s \in connected[Dom(s)]
|
||||
/\ domainPhase[Dom(s)] = "Ready"
|
||||
/\ SessionFingerprint[s] = DomainFingerprint[Dom(s)]
|
||||
|
||||
AuthenticatedRequestsOnly ==
|
||||
\A r \in Requests : requestPhase[r] # "Unused" =>
|
||||
/\ CertificateValid[r]
|
||||
/\ SignatureValid[r]
|
||||
|
||||
HostExecutionAuthorized ==
|
||||
\A r \in hostRunning :
|
||||
/\ requestPhase[r] = "Executing"
|
||||
/\ approvalSource[r] \in {"AutoRule", "Developer"}
|
||||
/\ approvedCommand[r] \in Commands
|
||||
|
||||
AutoApprovalNoShell ==
|
||||
\A r \in Requests : approvalSource[r] = "AutoRule" =>
|
||||
/\ approvedCommand[r] = RequestCommand[r]
|
||||
/\ ~CommandRequiresShell[approvedCommand[r]]
|
||||
|
||||
TerminalIsolation ==
|
||||
\A s \in Sessions : sessionPhase[s] = "Prompting" =>
|
||||
terminalRoute[s] = "Prompt"
|
||||
|
||||
NoOrphanHostProcess ==
|
||||
\A r \in hostRunning :
|
||||
LET s == ReqSession(r) IN
|
||||
/\ activeRequest[s] = r
|
||||
/\ sessionPhase[s] = "HostExecuting"
|
||||
/\ connectionState[s] = "Connected"
|
||||
|
||||
StateAnchorOwnership ==
|
||||
\A d \in Domains : stateAnchorState[d] = "Mounted" =>
|
||||
coordinatorState[d] \in {"Starting", "Ready", "Stopping"}
|
||||
|
||||
Safety ==
|
||||
/\ TypeOK
|
||||
/\ ConfigurationConfinement
|
||||
/\ OrderedNetworkStartup
|
||||
/\ PublishedDomainsReady
|
||||
/\ RegisteredSessionsConnected
|
||||
/\ AttachedSessionsValid
|
||||
/\ AuthenticatedRequestsOnly
|
||||
/\ HostExecutionAuthorized
|
||||
/\ AutoApprovalNoShell
|
||||
/\ TerminalIsolation
|
||||
/\ NoOrphanHostProcess
|
||||
/\ StateAnchorOwnership
|
||||
|
||||
(***************************************************************************
|
||||
Liveness properties. TLC configurations may check these separately from the
|
||||
safety sweep when the state space grows.
|
||||
***************************************************************************)
|
||||
|
||||
SessionEventuallyResolves ==
|
||||
\A s \in Sessions :
|
||||
sessionPhase[s] = "New" ~>
|
||||
sessionPhase[s] \in {"Attached", "Closed", "Failed"}
|
||||
|
||||
RequestEventuallyResolves ==
|
||||
\A r \in Requests :
|
||||
requestPhase[r] = "Verified" ~>
|
||||
requestPhase[r] \in TerminalRequestPhases
|
||||
|
||||
PromptEventuallyRestoresTerminal ==
|
||||
\A s \in Sessions :
|
||||
sessionPhase[s] \in {"Prompting", "HostExecuting"} ~>
|
||||
terminalRoute[s] \in {"Sandbox", "Detached"}
|
||||
|
||||
EmptyDomainEventuallyStops ==
|
||||
\A d \in Domains :
|
||||
(<>[](Interested(d) = {})) => <>(domainPhase[d] = "Absent")
|
||||
|
||||
=============================================================================
|
||||
|
|
@ -1,446 +1,264 @@
|
|||
# Lazaret: Unified Sandbox Quarantine & Host Escalation CLI
|
||||
## Architecture & Technical Specification
|
||||
# Lazaret Specification
|
||||
|
||||
**Lazaret** is a secure developer-experience (DX) tool written in Rust. It orchestrates rootless, gVisor-sandboxed container environments for AI agents, isolating them from the host network and system namespace by default, while providing an interactive, human-in-the-loop (HITL) **Host Escalation** protocol for controlled actions.
|
||||
This document is the normative product specification for Lazaret. It defines the
|
||||
problem, stable vocabulary, architecture, and requirements. The
|
||||
[TLA+ application protocol model](protocol/Lazaret.tla) defines lifecycle and communication
|
||||
semantics. The [implementation plan](implementation-plan.md) records current
|
||||
technology choices, configuration schema, sequencing, and validation work.
|
||||
Experimental results and historical rationale belong in the [journal](journal.md).
|
||||
|
||||
## Glossary
|
||||
## 1. Problem Statement
|
||||
|
||||
| Term | Definition |
|
||||
| :-------------------- | :--------- |
|
||||
| **Agent Workspace** | The bind-mounted host project directory inside the **Agent Sandbox** (at `/workspace`). |
|
||||
| **Agent Sandbox** | The gVisor (`runsc`) container running an agent profile, scoped uniquely to a **Domain Anchor** (representing a specific agent and canonical workspace path pairing). |
|
||||
| **Client Session** | A dynamic pairing of an active host `lazaret` CLI process and its corresponding containerized shell session inside the **Agent Sandbox**, secured by a **Session Anchor** and a **Trust Anchor**. |
|
||||
| **Session Anchor** | The ephemeral asymmetric keypair $(K_{pub\_proc}, K_{priv\_proc})$ generated by a host CLI process upon startup, used to cryptographically sign process-level escalation requests. |
|
||||
| **Session Coordinator**| The background-detached, decentralized host coordinator process that manages the container, **Network Sidecar**, and **State Anchor** overlays for a specific **Domain Anchor**. |
|
||||
| **Network Sidecar** | The shell-less gateway container (from `scratch`) running the Netlink router under `runc`, scoped uniquely to a **Domain Anchor** to enforce network egress policies. |
|
||||
| **Git Sentry** | The hybrid nesting layout consisting of the read-write mounted host `.git` under `/workspace/.git`, paired with a transient `tmpfs` mask over `hooks`, and a read-only bind mount of `.git/config`. |
|
||||
| **State Anchor** | The host-side `fuse-overlayfs` structure (`usr_changes`, `usr_work`, `usr_merged`) that merges a read-only container base OS directory with a persistent, writable host layer (scoped under the **Domain Root**), bind-mounted over container system paths (e.g., `/usr`) to preserve state for that specific agent-workspace pairing across container restarts. |
|
||||
| **Trust Anchor** | The in-memory, local ephemeral Certificate Authority (CA) managed by the host orchestrator or **Session Coordinator** on startup to issue short-lived cryptographic client certificates to sandboxes for secure mutual TLS (mTLS) identity verification over the **Escalation Socket**. |
|
||||
| **Host Escalation** | The secure UDS channel (secured by mTLS via the **Trust Anchor**) from the **Escalation Client** to the host TUI for approved host actions. |
|
||||
| **Escalation Client** | The statically compiled `lazaret-escalate` binary, volume-mounted read-only inside the sandbox. |
|
||||
| **Escalation Socket** | The UNIX domain socket scoped to a **Domain Anchor** used for routing **Host Escalation** commands. |
|
||||
| **Domain Anchor** | The deterministic, cryptographically derived boundary (representing the pairing of a specific agent profile and a unique absolute host workspace path) used to scope and namespace the sandbox container name, the **Session Coordinator**'s control socket, the advisory lockfile, and the persistent **State Anchor** storage. |
|
||||
| **Domain Root** | The directory on the host filesystem (`<data_root>/domains/<domain_anchor_hash>/`) under the **Storage Root** where the **State Anchor** and other domain-scoped files are stored. |
|
||||
| **Storage Root** | The host directory configured via the `data_root` property, serving as the base folder for all persistent agent profiles and domain storage. |
|
||||
AI coding agents need broad local capabilities: they run arbitrary tools, modify a
|
||||
project, use Git, install dependencies, and access selected network services. Code
|
||||
produced or invoked by an agent is not trusted to run directly on the developer's
|
||||
host. A conventional container reduces risk, but poor terminal behavior, transient
|
||||
tool state, restricted Git access, and manual setup can make the safe path too
|
||||
inconvenient for routine work.
|
||||
|
||||
## Fixed In-Container Paths
|
||||
Lazaret's north star is:
|
||||
|
||||
Lazaret-managed files and sockets inside every Agent Sandbox live under `/run/lazaret/`:
|
||||
> Make isolated agent execution the easiest way to work, while ensuring that every
|
||||
> transition from the sandbox to host authority is explicit, attributable, and
|
||||
> controlled by the developer.
|
||||
|
||||
| Path | Description |
|
||||
| :---------------------- | :---------- |
|
||||
| `/run/lazaret/escalate` | The Escalation Client binary (volume-mounted, read-only). |
|
||||
| `/run/lazaret/escalate.sock`| The Escalation Socket. |
|
||||
| `/run/lazaret/certs` | Ephemeral Trust Anchor client certificates (memory-backed tmpfs, read-only). |
|
||||
Lazaret therefore provides a rootless gVisor sandbox with a bidirectionally shared
|
||||
workspace, persistent sandbox-local tooling, controlled network egress, and a
|
||||
human-in-the-loop host escalation path.
|
||||
|
||||
The **Agent Workspace** is mounted at `/workspace`.
|
||||
### 1.1 Trust Model
|
||||
|
||||
---
|
||||
The following components are trusted:
|
||||
|
||||
## 1. System Objectives & Goals
|
||||
- The developer and host operating system.
|
||||
- The Lazaret CLI and the domain's Session Coordinator.
|
||||
- Host configuration selected by the developer.
|
||||
- The Network Sidecar and the policy given to it by Lazaret.
|
||||
|
||||
The primary objectives of Lazaret are to establish a secure, rootless, and isolated sandbox for untrusted AI agent execution while maintaining a functional developer experience through secure host escalation, persistent state, and real-time Git synchronization.
|
||||
The following components are untrusted:
|
||||
|
||||
### 1.1 Core Objectives
|
||||
* **Deep Virtualization-Based Isolation:** Run AI agent workspaces in rootless Podman containers utilizing the **gVisor (`runsc`)** sandbox runtime to establish a host-container kernel boundary.
|
||||
* **Network Sidecar:** Route container traffic through a statically compiled, shell-less network gateway container (`scratch`) running a custom Netlink router that dynamically applies egress policies.
|
||||
* **Persistent State:** Provide configurable persistence folders via a **State Anchor** to persist designated system and workspace directories across container restarts.
|
||||
* **Git Hook & Config Protection:** Enforce the **Git Sentry** isolation pattern to block git hook writes and config pager modifications, while permitting standard local git commands and tracking upstream configuration changes.
|
||||
* **Host Escalation:** Provide a secure UNIX Domain Socket (UDS) **Escalation Socket** channel, verified via the **Trust Anchor** (using mTLS), to route structured command execution requests from the **Escalation Client** inside the Agent Sandbox to the host orchestrator for user approval or modification.
|
||||
- The agent and every process it launches.
|
||||
- Code, tools, and package installation scripts executed inside the Agent Sandbox.
|
||||
- Data received from the network by sandboxed processes.
|
||||
|
||||
---
|
||||
All Client Sessions in the same Domain intentionally share an Agent Sandbox and
|
||||
are one sandbox trust domain. Session identity distinguishes ownership and routes
|
||||
terminal interactions; it is not an isolation boundary between processes in that
|
||||
sandbox.
|
||||
|
||||
## 2. Architecture & Data Flow
|
||||
The Agent Workspace is intentionally writable. Lazaret protects the host outside
|
||||
the explicitly mounted workspace and volumes; it does not protect workspace files
|
||||
from modification or deletion by the agent.
|
||||
|
||||
Host configuration is read only by host Lazaret processes. A configuration file
|
||||
must not be mounted into, or located beneath a host path mounted into, the Agent
|
||||
Sandbox.
|
||||
|
||||
## 2. Glossary
|
||||
|
||||
These terms and identifiers are stable across the specification, protocol model,
|
||||
implementation plan, logs, and user interfaces.
|
||||
|
||||
| Term | Definition |
|
||||
| :--- | :--- |
|
||||
| **Agent Profile** | A developer-selected host configuration describing an agent image and its granted storage, network, environment, and escalation capabilities. |
|
||||
| **Agent Workspace** | The canonical host project directory mounted read-write at `/workspace` in the Agent Sandbox. |
|
||||
| **Agent Sandbox** | The rootless gVisor container in which an agent and its child processes execute. One sandbox is shared by all Client Sessions in a Domain. |
|
||||
| **Agent Sandbox ID** | A runtime-unique identifier assigned to one Agent Sandbox instance and bound to its Domain protocol identity. |
|
||||
| **Client Session** | One active host Lazaret CLI process and the interactive or non-interactive command it attaches to the Agent Sandbox. |
|
||||
| **Session ID** | A cryptographically random identifier unique to one Client Session. |
|
||||
| **Session Anchor** | Ephemeral session credentials that bind protocol messages to a registered Session ID and route them to the owning host CLI. |
|
||||
| **Session Coordinator** | The ephemeral host process that exclusively owns and coordinates a Domain's runtime resources and Client Sessions. |
|
||||
| **Domain** | The shared runtime and persistent-state scope for one `(Agent Profile, canonical Agent Workspace)` pair. |
|
||||
| **Domain ID** | A deterministic identifier derived from the Agent Profile name and canonical Agent Workspace path. It namespaces Domain resources but is not an authentication credential. |
|
||||
| **Domain Root** | The host-only directory containing persistent state and metadata for one Domain. |
|
||||
| **Network Sidecar** | The trusted, shell-less gateway container that establishes the Domain's network policy before the Agent Sandbox starts. |
|
||||
| **Network Sidecar ID** | A runtime-unique identifier assigned to the Network Sidecar instance owned by a Domain. |
|
||||
| **Git Sentry** | The protective mount layout that preserves bidirectional workspace and Git access while preventing writes to the host's default Git hooks and repository configuration. |
|
||||
| **State Anchor** | The Domain-scoped persistent writable filesystem layer merged over selected paths from the sandbox base image. |
|
||||
| **Trust Anchor** | The ephemeral Domain-scoped certificate authority used to authenticate Lazaret protocol peers. |
|
||||
| **Escalation Client** | The Lazaret-provided executable through which a sandboxed process requests a host action. |
|
||||
| **Escalation Request ID** | A cryptographically random identifier used to correlate one host escalation request and response. |
|
||||
| **Host Escalation** | A request by a sandboxed process for Lazaret to execute a developer-approved or auto-approved action with host authority. |
|
||||
| **Configuration Fingerprint** | A cryptographic digest of the validated host configuration that created or joined a running Domain. |
|
||||
| **Storage Root** | The trusted host directory beneath which Lazaret stores Domain Roots and profile-scoped state. |
|
||||
|
||||
## 3. Architecture
|
||||
|
||||
### 3.1 System Boundaries
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Host Machine
|
||||
Orchestrator[Lazaret Host CLI Process]
|
||||
Config[~/.config/lazaret/config.toml]
|
||||
Storage[Sandbox Storage Root / NVMe]
|
||||
TUI[Interactive HITL TUI Prompt]
|
||||
UDS_Server[Unix Domain Socket Server]
|
||||
flowchart LR
|
||||
subgraph Host[Trusted host]
|
||||
CLI[Client CLI]
|
||||
Config[Host configuration]
|
||||
HostWorkspace[Agent Workspace]
|
||||
Coordinator[Session Coordinator]
|
||||
State[State Anchor]
|
||||
Gateway[Network Sidecar]
|
||||
HostExec[Approved host process]
|
||||
end
|
||||
|
||||
subgraph Container Isolation Boundary
|
||||
subgraph Gateway Sidecar (scratch - runc)
|
||||
RouteProcess[Netlink Router Process]
|
||||
end
|
||||
|
||||
subgraph Agent Sandbox (Debian Trixie - runsc/gVisor)
|
||||
AgentProcess[Agent Process / CLI]
|
||||
EscalateCLI[lazaret-escalate CLI]
|
||||
end
|
||||
subgraph Sandbox[Untrusted Agent Sandbox - rootless runsc]
|
||||
Agent[Agent and child processes]
|
||||
Workspace[/workspace mount]
|
||||
Escalate[Escalation Client]
|
||||
end
|
||||
|
||||
Orchestrator -->|Loads| Config
|
||||
Orchestrator -->|Initializes Storage| Storage
|
||||
Orchestrator -->|Spins up Sidecar| RouteProcess
|
||||
Orchestrator -->|Launches Sandbox| AgentProcess
|
||||
|
||||
RouteProcess -->|Configures Netlink| GatewaySidecarNetwork
|
||||
AgentProcess -->|Binds Network Namespace| RouteProcess
|
||||
|
||||
AgentProcess -->|Requests host command| EscalateCLI
|
||||
EscalateCLI -->|JSON Request via UDS| UDS_Server
|
||||
UDS_Server -->|Triggers Prompt| TUI
|
||||
TUI -->|User Approval / Modification| Orchestrator
|
||||
Orchestrator -->|Executes Command on Host| HostEnv[Host Environment]
|
||||
HostEnv -->|Sends Stdout / Stderr| UDS_Server
|
||||
UDS_Server -->|JSON Response| EscalateCLI
|
||||
Config --> CLI
|
||||
HostWorkspace <--> Workspace
|
||||
CLI <--> Coordinator
|
||||
Coordinator --> State
|
||||
Coordinator --> Gateway
|
||||
Coordinator --> Agent
|
||||
Agent <--> Workspace
|
||||
Agent --> Escalate
|
||||
Escalate --> Coordinator
|
||||
Coordinator --> HostExec
|
||||
Agent --> Gateway
|
||||
Gateway --> Network[Permitted destinations]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Configuration Contract
|
||||
|
||||
Configuration files are parsed hierarchically (lowest precedence to highest):
|
||||
1. User-wide global default: `~/.config/lazaret/config.toml`
|
||||
2. Workspace local override: `./lazaret.toml` (in the active working directory)
|
||||
3. Direct override command-line argument: `--config <file>`
|
||||
|
||||
A configuration template demonstrating representative agent profiles is located in the repository as `example.toml`.
|
||||
|
||||
### 3.1 Schema Contract Specification
|
||||
|
||||
#### Section `[storage]`
|
||||
| Property | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `data_root` | String | Yes | Absolute path on the host where persistent directories (home, usr, etc.) are initialized and mounted. |
|
||||
|
||||
#### Section `[network]`
|
||||
| Property | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `default_allow` | Array of Strings | Yes | Global whitelist of destination CIDR networks permitted for all containers. |
|
||||
| `default_deny` | Array of Strings | Yes | Global blacklist of destination CIDR networks (e.g., RFC 1918) blackholed for all containers. |
|
||||
|
||||
#### Section `[escalation]`
|
||||
| Property | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `enabled` | Boolean | Yes | Activates or suspends the host-side **Escalation Socket** listener during the lifecycle of the CLI command. |
|
||||
| `socket_dir` | String | Yes | Path on the host filesystem (e.g. `/run/user/1000/lazaret/` or `/tmp/lazaret`) where profile-specific Unix **Escalation Socket**s are created. |
|
||||
| `auto_approve` | Array of Strings | No | List of user-defined CLI commands allowed to execute on the host automatically without interactive confirmation. |
|
||||
|
||||
#### Section `[profiles.<profile_name>]`
|
||||
| Property | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `image` | String | Yes | The fully-qualified OCI container image to pull and launch. To prevent background updates and local host state drift, specific image tags or SHA-256 digests should be pinned. |
|
||||
| `persistent_dirs` | Array of Strings | No | Paths inside the container copied out and persisted under the host's storage directory (e.g. `/usr`, `/var`). |
|
||||
| `allow_egress` | Array of Strings | No | CIDRs allowed specifically for this agent, merged with the default global network whitelist. |
|
||||
| `env` | Map of Key-Value | No | Environment variables forwarded to the container. Setting a value prefix of `env:VAR_NAME` pulls dynamically from the host. |
|
||||
| `volumes` | Array of Objects | No | Custom volumes mapped from host to container. Hosts support the `{profile_dir}` expansion. |
|
||||
|
||||
#### Custom Volume Object
|
||||
* `host`: String. Host directory or file path. May use `{profile_dir}` which resolves to `<data_root>/<profile_name>/`.
|
||||
* `container`: String. Target path inside the container's directory tree.
|
||||
* `mode`: String. Mount properties (e.g., `z` for SELinux sharing, `ro` for read-only bind mounts).
|
||||
|
||||
### 3.2 Variable Expansion, Templating & Sanitation Specification
|
||||
|
||||
Lazaret implements a custom, zero-dependency, safe variable substitution engine for configuration parsing.
|
||||
|
||||
#### 3.2.1 Linux-Only Tilde (`~`) Expansion
|
||||
Lazaret supports only Linux-based host environments. Path resolution and tilde (`~`) expansion must conform to Linux directory standards:
|
||||
* Lazaret retrieves the host user's home directory by reading the `$HOME` environment variable, falling back to query the host's `/etc/passwd` entry if `$HOME` is unpopulated.
|
||||
* **Strict Validation:** Tilde is only expanded if it is the absolute first character of the path string (e.g. `~/config` resolves to `/home/user/config`, but `some/path/~` is treated as a literal directory named `~`).
|
||||
|
||||
#### 3.2.2 Expansion Tokens
|
||||
Lazaret parses and expands a small, strictly defined whitelist of local variables inside configuration fields:
|
||||
* `{profile_name}`: Evaluates to the lowercase alphanumeric name of the active agent profile.
|
||||
* `{profile_dir}`: Expands dynamically on the host to: `{data_root}/{profile_name}`.
|
||||
* `{domain_hash}`: Evaluates to the 12-character SHA-256 hexadecimal hash representing the Domain Anchor of the active `(agent_name, canonical_workspace_path)` pair.
|
||||
* `{domain_dir}`: Expands dynamically on the host to the Domain Root directory: `{data_root}/domains/{domain_hash}`.
|
||||
* `{workspace_dir}`: Evaluates to the current host working directory.
|
||||
|
||||
#### 3.2.3 Sanitation & Directory Traversal Protection
|
||||
Lazaret enforces three structural validation boundaries for configuration paths:
|
||||
|
||||
1. **Profile Name Sanitation:**
|
||||
Profile names must strictly match the regex pattern: `^[a-z0-9_-]{2,32}$`. Any profile name containing dots (`.`), slashes (`/`, `\`), or shell metacharacters is immediately rejected, preventing injection attacks when creating paths like `{data_root}/{profile_name}`.
|
||||
|
||||
2. **Path Traversal Interception:**
|
||||
Before resolving any expanded path, Lazaret passes the resulting string to a sanitation function that checks for the presence of relative path segments. If a resolved path contains a dot-dot segment (`..`), the execution is aborted immediately with a security panic, preventing the agent from escaping the storage boundaries.
|
||||
|
||||
3. **Absolute Path Verification:**
|
||||
All host paths used for mounts, storage, and volumes are canonicalized (resolving all symlinks and relative segments to absolute paths) before being passed to Podman execution commands. This prevents relative symlink tricks or path obfuscation from bypassing rootless boundaries.
|
||||
|
||||
---
|
||||
|
||||
## 4. Key Security & Functional Mechanisms
|
||||
|
||||
### 4.1 Git Hook & Config Isolation (Git Sentry)
|
||||
Lazaret isolates the container's Git environment and prevents modifications to host configuration and hook files using a nested ("Inside Mount") layout called **Git Sentry**.
|
||||
|
||||
When a git configuration is updated on the host, Git atomically unlinks and replaces `.git/config`. A direct single-file read-only bind mount of `.git/config` remains bound to the stale, unlinked file inode inside the container namespace (the Unix stale inode problem). To ensure that the container namespace dynamically reflects host-side updates, the read-only file mount must be nested inside a parent read-write bind-mounted directory, allowing the kernel to resolve the updated inode path dynamically.
|
||||
|
||||
#### 4.1.1 The Decoupling Protocol
|
||||
The host orchestrator constructs a nested virtual layout inside the container's mount namespace to isolate the Git configuration and hook directories:
|
||||
|
||||
1. **Agent Workspace Mount:** The active **Agent Workspace** is bind-mounted read-write at `/workspace` (utilizing the `:z` flag for rootless SELinux compatibility). This exposes the host's `.git/index` to enable standard git status and staging operations from within the container.
|
||||
2. **Hooks Sandbox Mount:** A memory-backed `tmpfs` volume is mounted over `/workspace/.git/hooks` inside the container to isolate hook creation and execution to transient container memory.
|
||||
3. **Config Read-Only Mount:** The host's `.git/config` file is bind-mounted read-only over `/workspace/.git/config`. Because this mount is nested inside the parent bind-mounted `/workspace` directory, parent directory resolution propagates host-side atomic replacements of `.git/config` to the container namespace, bypassing the stale inode problem.
|
||||
|
||||
#### Filesystem Mount Mapping Table
|
||||
| Path in Agent Workspace | Mount Type | Properties / Permissions | Purpose |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `/workspace` | Bind Mount | Read / Write (`z` shared) | Shares the active project folder with the Agent Sandbox. |
|
||||
| `/workspace/.git/hooks` | `tmpfs` | Memory-only, transient | Masks the git hooks folder inside the Agent Workspace, blocking direct host hook writes. |
|
||||
| `/workspace/.git/config`| Bind Mount | Read-Only (`ro`, `z` shared) | Prevents modification of git configurations, while allowing dynamic updates from host. |
|
||||
| `/workspace/.vscode` | `tmpfs` | Memory-only | Masks user's host VSCode preferences inside the Agent Workspace. |
|
||||
| `/workspace/.idea` | `tmpfs` | Memory-only | Masks user's host IntelliJ configurations inside the Agent Workspace. |
|
||||
|
||||
#### 4.1.2 Security & Operational Guarantees
|
||||
* **Immutable Target Integrity:** Write or append attempts targeting `/workspace/.git/config` inside the container must fail at the kernel level with `EROFS`.
|
||||
* **Deletion Blocked:** Deletion or unlinking of `/workspace/.git/config` inside the container must be blocked by the kernel VFS layer with `EBUSY` (Device or resource busy).
|
||||
* **Instant Configuration Visibility:** Updates to `.git/config` on the host must propagate instantly to the container's `/workspace/.git/config` mount via parent directory resolution.
|
||||
* **State Propagation:** Branch modifications and reference writes made by the container to `/workspace/.git/HEAD` and `/workspace/.git/refs` must propagate to the host in real-time.
|
||||
|
||||
### 4.2 Sandbox Git Worktree/Branch Isolation
|
||||
Lazaret isolates sandbox git actions to a dedicated branch namespace:
|
||||
* **Dedicated Branch:** Lazaret must automatically derive an agent-specific branch name (`lazaret/agent-<name>`) from the active host commit.
|
||||
* **Stash Protection:** The host workspace active working tree must remain unmodified during sandbox execution. Changes made by the agent within the sandbox must be committed or written only to the dedicated branch, allowing the user to inspect differences on the host with `git diff` or merge changes explicitly.
|
||||
|
||||
### 4.3 State Scoping & Multi-Instance Lifecycle via Domain Anchor
|
||||
|
||||
Lazaret scopes persistent state, socket routing, and container lifecycles to a cryptographically derived **Domain Anchor**.
|
||||
|
||||
#### 4.3.1 Domain Anchor Derivation Protocol
|
||||
For every CLI invocation, the host orchestrator must compute a unique, deterministic 12-character hexadecimal identifier (the `domain_hash`) representing the active boundary.
|
||||
The hash is derived as follows:
|
||||
1. Retrieve the lowercase alphanumeric agent profile name.
|
||||
2. Canonicalize the absolute filesystem path of the active **Agent Workspace** on the host.
|
||||
3. Compute the SHA-256 digest of the UTF-8 encoded string: `"<agent_name>:<canonical_workspace_path>"`.
|
||||
4. Truncate the resulting hexadecimal string to its first 12 characters.
|
||||
|
||||
This identifier must be used to isolate host-side resources. The resource mapping rules are defined below:
|
||||
|
||||
| Resource Type | Host Pathing Rule | Format / Name Scheme |
|
||||
| :--- | :--- | :--- |
|
||||
| **Domain Root** | `{data_root}/domains/{domain_hash}/` | Path to persistent domain storage. |
|
||||
| **Control Socket** | `/tmp/lazaret_coord_{domain_hash}.sock` | Unix socket for Coordinator control. |
|
||||
| **Lock File** | `/tmp/lazaret_coord_{domain_hash}.lock` | Path used for advisory locking. |
|
||||
| **Container Name** | `lazaret-coord-{domain_hash}` | Scoped Podman container instance name. |
|
||||
|
||||
---
|
||||
|
||||
#### 4.3.2 Host-Side Directory Layout
|
||||
The State Anchor directory structure is established on the host under the **Domain Root** (`{data_root}/domains/{domain_hash}/`):
|
||||
* `usr_changes/`: The persistent, writable layer (`upperdir`) storing user-installed packages and modifications. Initially empty.
|
||||
* `usr_work/`: The private metadata workspace directory (`workdir`) required by overlayfs.
|
||||
* `usr_merged/`: The virtual, merged mount point directory (`mergeddir`) exposed to the sandbox.
|
||||
|
||||
---
|
||||
|
||||
#### 4.3.3 State Anchor Mounting Protocol
|
||||
The host orchestrator establishes a user-space overlay filesystem (such as `fuse-overlayfs`) on the host within the rootless user namespace before launching the container:
|
||||
1. **Locate Base Image Mount:** The orchestrator must query the OCI runtime to resolve the base-image read-only root mount path on the host.
|
||||
2. **Mount User-Space Overlay:** The orchestrator must mount a user-space overlay filesystem (e.g. `fuse-overlayfs`) within the rootless user namespace, using the base-image mount `/usr` directory as the read-only lower directory (`lowerdir`), and the domain-scoped directories `usr_changes/` and `usr_work/` as the `upperdir` and `workdir`, respectively.
|
||||
3. **Sandbox Mount Injection:** The orchestrator must launch the container with the merged user-space overlay directory (`usr_merged/`) bind-mounted read-write over the container's `/usr` path.
|
||||
|
||||
---
|
||||
|
||||
#### 4.3.4 Image Drift Mitigation & Podman Compatibility
|
||||
The State Anchor overlay structure dynamically tracks updates to the underlying OCI container base image.
|
||||
|
||||
* **Base Image Update Inheritance:** Upon pulling an updated base image, the orchestrator uses the new image's `/usr` directory as the read-only `lowerdir` for subsequent user-space overlay mounts, preserving modifications in `usr_changes/`.
|
||||
* **Podman Integration Compatibility Exclusion:** When applying State Anchor overlays over system directories (such as `/etc`), the orchestrator must exclude runtime-injected configuration files (specifically `/etc/resolv.conf`, `/etc/hosts`, and `/etc/hostname`) from the overlay merge to preserve container DNS and hostname resolution.
|
||||
|
||||
---
|
||||
|
||||
#### 4.3.5 Ephemeral Session Coordinator & Multi-Instance Multiplexing
|
||||
|
||||
Lazaret coordinates concurrent CLI processes interacting with the same **Domain Anchor** using a decentralized, background-detached **Session Coordinator** pattern.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
%% Nodes
|
||||
subgraph Host OS Namespace
|
||||
CLI_A["CLI Session A<br>(Leader)"]
|
||||
CLI_B["CLI Session B<br>(Follower)"]
|
||||
|
||||
Lock["Advisory Lockfile<br>(LOCK_EX)"]
|
||||
|
||||
Coord["Session Coordinator<br>(Background Process)"]
|
||||
Socket["Control Socket<br>(AF_UNIX)"]
|
||||
|
||||
Overlay["State Anchor<br>(fuse-overlayfs)"]
|
||||
end
|
||||
|
||||
subgraph gVisor Sandbox Namespace
|
||||
Container["gVisor Container<br>(lazaret-coord-{hash})"]
|
||||
end
|
||||
|
||||
%% Interactions (Leader Sequence)
|
||||
CLI_A -->|1. Acquires LOCK_EX| Lock
|
||||
CLI_A -->|2. Spawns| Coord
|
||||
Coord -->|3. Mounts usr_merged| Overlay
|
||||
Coord -->|4. Boots| Container
|
||||
Coord -->|5. Binds socket| Socket
|
||||
Coord -.->|6. Releases LOCK_EX| Lock
|
||||
|
||||
%% Interactions (Follower Sequence)
|
||||
CLI_B -->|1. Blocks on LOCK_EX| Lock
|
||||
Lock -.->|2. Unblocks after socket bind| CLI_B
|
||||
CLI_B -->|3. Connects| Socket
|
||||
CLI_B -->|4. podman exec| Container
|
||||
```
|
||||
|
||||
##### Step 1: The "Hold-to-Bind" Advisory Locking Protocol
|
||||
During CLI startup, the process must handle concurrency via Linux advisory file locking (`flock`):
|
||||
1. **Acquire Lock:** The process attempts to acquire an exclusive, non-blocking lock (`LOCK_EX | LOCK_NB`) on `/tmp/lazaret_coord_{domain_hash}.lock`.
|
||||
2. **If Successful (Leader):**
|
||||
* The process must check if the `Control Socket` exists. If not, it must spawn the **Session Coordinator** in the background, fully detached from the parent's process group (e.g., creating a new session via standard POSIX `setsid` or equivalent OS APIs, detaching it from the controlling terminal).
|
||||
* The leader process must **poll** the host filesystem for the `Control Socket` to bind (up to 10 seconds).
|
||||
* The leader must not release its exclusive lock on the lockfile until the control socket has successfully bound and is ready to accept connections.
|
||||
3. **If Unsuccessful (Follower):**
|
||||
* The process blocks on the lock (`LOCK_EX`) until the leader completes coordinator setup and releases the lock.
|
||||
* Once unblocked, the follower immediately releases the lock (`LOCK_UN`) and safely connects to the active `Control Socket`.
|
||||
|
||||
##### Step 2: Reference Counted Teardown
|
||||
The Session Coordinator keeps an active map of connected CLI processes.
|
||||
* Whenever a CLI process terminates, it disconnects from the control socket.
|
||||
* When the active connection count on the Coordinator reaches **zero**, the Coordinator must initiate an automatic clean teardown: stopping the sandboxed container, unmounting the `fuse-overlayfs` State Anchor, and unlinking the control socket.
|
||||
|
||||
##### Step 3: Session Identity & Escalation Routing via Session Anchor
|
||||
To maintain exact terminal isolation and prevent spoofing or PID-reuse hijacking during interactive prompts:
|
||||
1. The host CLI process generates a **Session Anchor** (an ephemeral asymmetric keypair $(K_{pub\_proc}, K_{priv\_proc})$) and registers $K_{pub\_proc}$ alongside a unique `LAZARET_SESSION_ID` (represented by a cryptographically secure unique identifier) with the Session Coordinator.
|
||||
2. When launching a shell or command session inside the container (via `podman exec`), the host CLI process injects a cryptographically signed capability token or delegates a short-lived keypair derived from $K_{priv\_proc}$ into that specific process's environment.
|
||||
3. When the in-container **Escalation Client** (`lazaret-escalate`) triggers an escalation request, it must sign the JSON payload (including the request ID and `LAZARET_SESSION_ID`) with the injected session credential.
|
||||
4. The Session Coordinator receives the request over mTLS, verifies the container certificate, retrieves the registered $K_{pub\_proc}$ matching the `LAZARET_SESSION_ID`, and validates the signature.
|
||||
5. Once verified, the Session Coordinator routes the interactive terminal user interface (TUI) prompt exclusively to the corresponding host terminal socket matching that verified ID.
|
||||
|
||||
---
|
||||
|
||||
### 4.4 Embedded State Store & Audit Trail Persistence
|
||||
|
||||
Lazaret implements a local persistence layer using an embedded transactional key-value store to manage session state transitions, lock statuses, and host escalation audit trails.
|
||||
|
||||
#### 4.4.1 Single-Process Exclusive Database Ownership
|
||||
The state database file (stored inside the Domain Root) is strictly owned and opened exclusively by the background-detached Session Coordinator process.
|
||||
* **Process-Level Exclusive Locking:** The embedded transactional key-value store enforces absolute process-level exclusive locking on the physical file. Any attempt by other processes (such as secondary CLI follower processes or standalone status query commands) to open the state file directly will fail immediately.
|
||||
* **IPC-Facade Query Pattern:** Consequently, all secondary CLI follower sessions, dashboard widgets, or status utilities must never access the state store directly on the filesystem. Instead, they must communicate with the active Session Coordinator over the Control Socket using Unix Domain Sockets, and the Coordinator must perform all queries, registrations, and updates on their behalf.
|
||||
|
||||
#### 4.4.2 Asynchronous Event Loop Thread Integration
|
||||
To prevent async event loop starvation from blocking database operations, all database transactions must be executed on a designated synchronous threadpool managed by the asynchronous runtime.
|
||||
|
||||
#### 4.4.3 Manual Schema Migration Protocol
|
||||
To maintain zero external C-dependencies for frictionless static compilation, the persistence layer utilizes a lightweight, typed key-value table layout.
|
||||
* **Manual Evolution:** The store does not support automated relational schema migration engines.
|
||||
* **Table-to-Table Copying:** Any schema evolution or configuration structural changes across feature updates must be coordinated by manual, programmatically written migration routines. These routines must query old tables, convert the underlying serializations, and write them to newer table versions inside a single atomic transaction.
|
||||
|
||||
#### 4.4.4 Division of Labor between State Store, Session Coordinator, and OS
|
||||
To prevent race conditions, memory leaks, and stale process tracking, the active runtime state is decoupled from passive storage:
|
||||
|
||||
1. **Passive ACID State Store:** Responsible solely for persisting completed session metadata, verified escalation approvals, and tamper-safe host command audit logs. It does not track real-time process heartbeats or actively coordinate system actions.
|
||||
2. **Active Session Coordinator:** Acts as the exclusive in-memory broker, managing async UNIX sockets, mTLS handshake parsing, active client connection reference counting, and executing actual container launches or workspace mounts.
|
||||
3. **OS Kernel Primitives:** Responsible for process death detection. The Session Coordinator and CLI processes utilize advisory file locking (`flock`) on the Lock File to manage active-leader lifecycles and clean up sockets upon process termination (including hard crashes such as `SIGKILL`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Agent-to-Host Escalation Protocol
|
||||
|
||||
### 5.1 Cryptographic Identity Verification & Message Transport
|
||||
The **Escalation Client** is placed inside the Agent Sandbox. It communicates with the host orchestrator over the **Escalation Socket** (UDS) utilizing a secure Mutual TLS (mTLS) handshake verified by the **Trust Anchor** to cryptographically prove sandbox identity.
|
||||
|
||||
The host orchestrator creates the **Escalation Socket** file inside a secure, host-private directory owned by the host developer (defined by `socket_dir` in the configuration) and bind-mounts only that socket into the Agent Sandbox, keeping it decoupled from the writable **Agent Workspace**.
|
||||
|
||||
#### 5.1.1 Cryptographic Identity Verification via Trust Anchor and Session Anchor
|
||||
Lazaret establishes process-level and container-level identities using the **Trust Anchor** and **Session Anchor** protocols:
|
||||
1. **Host CA Generation:** On startup, the host orchestrator instantiates the **Trust Anchor** as an in-memory, local ephemeral Certificate Authority (CA), generating a self-signed root CA certificate and private key in-memory.
|
||||
2. **Sandbox Client Certificate:** For each active sandbox container, the **Trust Anchor** generates a unique, short-lived client certificate and private key. This certificate encodes the active container's profile name and unique sandbox ID inside its Subject Alternative Name (SAN).
|
||||
3. **Mount Injection:** The host orchestrator mounts these Trust Anchor certificates into the container namespace using a memory-backed, read-only `tmpfs` volume (see Fixed In-Container Paths).
|
||||
4. **mTLS Handshake Check:** When the **Escalation Client** initiates a request, it performs a complete mutual TLS handshake over the **Escalation Socket**. The host verifies that the client certificate presented by the connection is signed by the active **Trust Anchor** root CA, and checks the profile SAN to confirm exact container identity before any payloads are parsed.
|
||||
5. **Session Signature Verification:** Once the mTLS connection is verified, the Session Coordinator reads the payload's `session_id`, retrieves the public key of the corresponding **Session Anchor** registered on startup, and verifies the payload's cryptographic `signature`. Only requests carrying a valid signature produced by that session's injected private key are approved for TUI routing or auto-approval.
|
||||
|
||||
#### 5.1.2 Command Request (Sandbox -> Host JSON payload over mTLS)
|
||||
* `request_id`: A cryptographically secure unique identifier.
|
||||
* `session_id`: The `LAZARET_SESSION_ID` corresponding to the requesting **Client Session**.
|
||||
* `profile`: The current agent profile requesting escalation.
|
||||
* `command`: Raw CLI command to run on the host.
|
||||
* `justification`: Detailed justification explaining why host resources are needed.
|
||||
* `signature`: Cryptographic signature of the request payload produced by the process's injected private credential, verifiable using the Session Anchor's registered public key.
|
||||
|
||||
#### 5.1.3 Command Response (Host -> Sandbox JSON payload over mTLS)
|
||||
* `request_id`: Matches the original command request ID.
|
||||
* `approved`: Boolean indicating if the user permitted execution.
|
||||
* `exit_code`: Process exit code (only set if approved is true).
|
||||
* `stdout`: Combined standard output text.
|
||||
* `stderr`: Combined standard error text.
|
||||
|
||||
### 5.2 HITL Interactive Console Interface & Whitelisting
|
||||
When a request is received on the host, the `lazaret` orchestrator must temporarily suspend standard TTY forwarding and render a dedicated interactive terminal user interface prompt. The interface must isolate the request ID, profile, justification, and the requested command, offering the user four distinct actions:
|
||||
* **[A] Approve & Run:** Executes the command on the host.
|
||||
* **[D] Deny Request:** Rejects the escalation, returning an access-denied response to the sandbox.
|
||||
* **[E] Edit Command:** Prompts the developer to edit the command in-place before execution.
|
||||
* **[Q] Abort:** Terminates the CLI orchestrator process, initiating a graceful shutdown of the container and **Network Sidecar** operations.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Request[Escalation Request from Sandbox] --> Metachar{Contains Shell<br>Metacharacters?}
|
||||
|
||||
%% Non-Shell Path
|
||||
Metachar -->|No| Whitelist{Matches Auto-Approve<br>Prefix Vector?}
|
||||
Whitelist -->|Yes| Auto[Auto-Approve Path]
|
||||
Whitelist -->|No| Prompt[Force Human HITL Prompt]
|
||||
|
||||
%% Shell Path
|
||||
Metachar -->|Yes| Prompt
|
||||
|
||||
%% Execution Paths
|
||||
Auto -->|Executes Directly| Execve[Direct execve Execution<br>No shell spawned]
|
||||
|
||||
Prompt -->|User Approves| ExecType{Has Shell<br>Metacharacters?}
|
||||
Prompt -->|User Denies| Deny[Return Access Denied]
|
||||
|
||||
ExecType -->|No| Execve
|
||||
ExecType -->|Yes| ShellExec[Execute in Host Shell]
|
||||
```
|
||||
|
||||
#### 5.2.1 Whitelisting & Injection Prevention (Argument Vector Prefix Matching)
|
||||
The orchestrator implements a command validation pipeline:
|
||||
|
||||
1. **Shell Metacharacter Auto-Approve Bypass:**
|
||||
Any requested command containing shell-interpreted metacharacters, control characters, or redirections (including `;`, `|`, `&`, `$()`, `` ` ` ``, `>`, `<`, `\n`, `\r`, and backslashes) must bypass the automatic whitelisting engine. Such commands must require interactive human-in-the-loop (HITL) approval to prevent arbitrary command chaining, background execution, or output redirection on the host.
|
||||
|
||||
2. **Argument Vector Prefix Whitelisting:**
|
||||
The requested command string must be parsed into an argument vector using a POSIX-compliant shell words parser to resolve quotes and escape spaces. This requested vector must be validated against the parsed vectors defined in the `auto_approve` configuration.
|
||||
To prevent argument injection vulnerabilities (where arbitrary parameters are appended to authorized binaries), the auto-approve verification must assert that the whitelisted vector is a strict prefix of the requested vector.
|
||||
* **Banned Commands:** Executables that permit arbitrary command redirection (including raw `git` or any `git` subcommands) must not be permitted on the auto-approve whitelist.
|
||||
|
||||
3. **Direct Execution:**
|
||||
Following verification, the orchestrator must pass the literal argument vector directly to the host's native process execution engine (such as the standard `execve` system call or equivalent system process APIs), launching the binary directly without invoking an intermediate host shell.
|
||||
|
||||
---
|
||||
|
||||
## 6. Functional & Non-Functional Requirements
|
||||
|
||||
### 6.1 Functional Requirements
|
||||
| ID | Requirement | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **FR-1** | TOML Profile Loading | Dynamically parses and loads config from `~/.config/lazaret/config.toml` or override paths. |
|
||||
| **FR-2** | Podman & gVisor Integration | Configures container executions using `--runtime=runsc` for sandboxes and `--runtime=runc` for sidecars. |
|
||||
| **FR-3** | Network Sidecar Lifecycle | Ensures the shell-less **Network Sidecar** running in `runc` is active before spawning any Agent Sandbox. |
|
||||
| **FR-4** | Git Hooks Isolation | Automatically isolates the container git environment using the Git Sentry. |
|
||||
| **FR-5** | Host Escalation | Establishes private host **Escalation Socket** listener with ephemeral mTLS certificate verification, parses JSON requests, and displays interactive HITL terminal prompt. |
|
||||
| **FR-6** | Domain Scoped Multiplexing | Orchestrates concurrent multi-session execution within a **Domain Anchor** utilizing a decentralized **Session Coordinator**, hold-to-bind locking, and identity-aware **Host Escalation** prompt routing. |
|
||||
|
||||
### 6.2 Non-Functional Requirements
|
||||
| ID | Requirement | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **NFR-1** | Minimal Sidecar Footprint | The **Network Sidecar** binary must compile statically (via `musl`) with minimal external dependencies to maintain a small footprint suitable for `scratch`. |
|
||||
| **NFR-2** | Robust Async Cancellation | Utilizes native asynchronous task cancellation and signal-handling mechanisms to cleanly close containers, clean up **Network Sidecar** instances, and close sockets on SIGINT or other termination events. |
|
||||
| **NFR-3** | Dynamic Terminfo Mapping | Locates host terminal terminfo configurations (Ghostty, Alacritty, Kitty) and bind-mounts them read-only into the Agent Sandbox. |
|
||||
| **NFR-4** | Zero Shell Footprint | The **Network Sidecar** container has no shell (`sh`, `bash`), preventing any remote execution/hijacking. |
|
||||
|
||||
---
|
||||
|
||||
## 7. Library Roles Map
|
||||
|
||||
The following table maps the abstract architectural roles used in this specification to their concrete library dependencies.
|
||||
|
||||
| Architectural Role | Abstract Prose Reference | Concrete Library | Justification |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Embedded Transactional Key-Value Store** | "embedded transactional key-value store" | `redb` (v4.1.0) | Pure-Rust, transactional store enabling static cross-compilation with robust ACID/MVCC features. |
|
||||
| **Terminal User Interface Library** | "interactive terminal user interface library" or "terminal user interface" | `ratatui` | Terminal rendering and layout primitives for the interactive prompt. |
|
||||
| **Asynchronous Runtime Event Loop** | "asynchronous runtime event loop" | `tokio` (v1.52.3) | Asynchronous execution runtime for socket and multiplexer coordination. |
|
||||
| **Cryptographically Secure Unique Identifiers** | "cryptographically secure unique identifier" or "unique session identifier" | `uuid` (v1.23.4) | Generation and validation of unique identifiers. |
|
||||
The Agent Sandbox uses gVisor's application-kernel isolation under rootless
|
||||
Podman. Rootless operation limits the host authority of the runtime, while gVisor
|
||||
interposes a separate userspace kernel between untrusted processes and the host
|
||||
kernel. The Network Sidecar is a separate trusted component and is not part of the
|
||||
untrusted agent environment.
|
||||
|
||||
### 3.2 Domain and Session Lifecycle
|
||||
|
||||
The Session Coordinator is the single owner of a Domain's Agent Sandbox, Network
|
||||
Sidecar, State Anchor, sockets, and active-session registry. Concurrent CLI
|
||||
processes for the same Domain join the existing coordinator and attach separate
|
||||
Client Sessions to the shared sandbox. Different Domains never share these
|
||||
resources.
|
||||
|
||||
The first Client Session starts the Domain. The coordinator prepares persistent
|
||||
state, establishes network policy, starts the Agent Sandbox, and then accepts the
|
||||
session. When no Client Sessions remain, it tears down transient resources while
|
||||
retaining Domain-scoped persistent state.
|
||||
|
||||
Startup, registration, cancellation, recovery, and teardown transitions are
|
||||
defined by the [TLA+ application protocol model](protocol/Lazaret.tla).
|
||||
|
||||
### 3.3 Filesystem and Git Model
|
||||
|
||||
The Agent Workspace is mounted read-write and changes propagate in both
|
||||
directions. Workspace content, the Git index, `HEAD`, references, and objects are
|
||||
therefore shared with the host in real time. Lazaret does not create a separate
|
||||
worktree or branch and does not promise that the host working tree remains
|
||||
unchanged.
|
||||
|
||||
Lazaret applies narrow protective mounts over host integration files that must not
|
||||
be modified by the agent:
|
||||
|
||||
- The host repository's `.git/config` is exposed read-only.
|
||||
- The host repository's `.git/hooks` is hidden behind sandbox-local transient
|
||||
storage.
|
||||
- Supported host editor configuration directories may be hidden behind
|
||||
sandbox-local transient storage.
|
||||
|
||||
These protections do not make the workspace immutable. Their purpose is to stop
|
||||
the sandbox from writing the protected host Git configuration and default hook
|
||||
directory while preserving normal Git operations and bidirectional source edits.
|
||||
|
||||
### 3.4 Persistent Sandbox State
|
||||
|
||||
The State Anchor preserves developer tooling and selected sandbox system paths for
|
||||
one Domain. It combines a read-only lower directory from the selected base image
|
||||
with a Domain-scoped writable upper directory.
|
||||
|
||||
Files not represented in the upper layer follow the selected base image. Files,
|
||||
directories, and whiteouts recorded in the upper layer remain authoritative when
|
||||
the lower image changes. This permits installed tools to survive restarts while
|
||||
allowing unmodified base-image content to follow the selected image.
|
||||
|
||||
Persistent state is sandbox state, not host configuration. It must not contain or
|
||||
override Lazaret's host control plane.
|
||||
|
||||
### 3.5 Network Isolation
|
||||
|
||||
The Network Sidecar establishes the Domain's egress routes before the Agent
|
||||
Sandbox starts. The sidecar alone receives the network-administration capability
|
||||
needed to install policy; the Agent Sandbox does not receive that capability.
|
||||
|
||||
Network policy is expressed as permitted and denied destination prefixes. Normal
|
||||
Linux longest-prefix routing determines the most specific matching route; an
|
||||
equal-prefix conflict must be rejected or resolved in favor of denial. IPv4 and
|
||||
IPv6 must either receive equivalent policy or an address family must be disabled.
|
||||
|
||||
The topology and policy-installation sequence must ensure that all external Agent
|
||||
Sandbox traffic traverses the configured enforcement path. Dynamic policy changes
|
||||
are not a requirement unless the implementation can apply them without weakening
|
||||
that property.
|
||||
|
||||
### 3.6 Host Escalation
|
||||
|
||||
The Escalation Client communicates with the Session Coordinator over a
|
||||
Domain-scoped host-private Unix socket. The Trust Anchor authenticates the Domain
|
||||
peer, and the Session Anchor binds the request to a registered Client Session. A
|
||||
verified request is routed only to that session's host CLI.
|
||||
|
||||
A host command may run only when the developer approves it interactively or when
|
||||
it matches an auto-approval rule in trusted host configuration. Auto-approved
|
||||
commands are launched directly as an argument vector without a shell. Rules match
|
||||
the complete vector by default; permission for arbitrary trailing arguments is an
|
||||
explicit property of an individual rule.
|
||||
|
||||
Interactive approval may allow the developer to approve, deny, or edit a request.
|
||||
The request must reach a terminal state and terminal ownership must return to the
|
||||
sandbox session after resolution.
|
||||
|
||||
Authentication, message correlation, approval, execution, cancellation, and
|
||||
terminal-routing transitions are defined by the
|
||||
[TLA+ application protocol model](protocol/Lazaret.tla).
|
||||
|
||||
## 4. Requirements
|
||||
|
||||
### 4.1 Security Requirements
|
||||
|
||||
| ID | Requirement |
|
||||
| :--- | :--- |
|
||||
| **SR-1** | Agent processes shall execute in a rootless gVisor sandbox and shall not receive host root authority. |
|
||||
| **SR-2** | Host configuration shall never be mounted into the Agent Sandbox or located beneath a host path mounted into it. |
|
||||
| **SR-3** | Host paths outside the Agent Workspace and developer-configured volumes shall not be accessible to the Agent Sandbox. |
|
||||
| **SR-4** | The sandbox shall be unable to modify the host repository's `.git/config` or default `.git/hooks` directory through the workspace mount. |
|
||||
| **SR-5** | The Network Sidecar shall establish network policy before the Agent Sandbox starts, and the Agent Sandbox shall not receive network-administration capability. |
|
||||
| **SR-6** | Network policy shall cover every enabled address family and shall deny equal-specificity policy conflicts. |
|
||||
| **SR-7** | A host action shall execute only after interactive developer approval or a match against a trusted host auto-approval rule. |
|
||||
| **SR-8** | Auto-approved host commands shall execute without a shell and shall permit trailing arguments only when the matched rule explicitly grants them. |
|
||||
| **SR-9** | Escalation requests shall be authenticated to a Domain and bound to a registered Session ID before routing or execution. |
|
||||
| **SR-10** | Domain sockets and coordination files shall reside in a host-private directory owned by the developer. |
|
||||
| **SR-11** | Runtime and persistent resources belonging to different Domain IDs shall remain isolated. |
|
||||
|
||||
### 4.2 Functional Requirements
|
||||
|
||||
| ID | Requirement |
|
||||
| :--- | :--- |
|
||||
| **FR-1** | Lazaret shall derive a stable Domain ID from the selected Agent Profile and canonical Agent Workspace. |
|
||||
| **FR-2** | Concurrent Client Sessions in one Domain shall share one coordinator, Agent Sandbox, Network Sidecar, and State Anchor. |
|
||||
| **FR-3** | Workspace edits and supported Git operations shall propagate bidirectionally between host and sandbox. |
|
||||
| **FR-4** | Selected sandbox paths shall persist across Domain restarts without copying Lazaret host configuration into the sandbox. |
|
||||
| **FR-5** | The coordinator shall route an escalation prompt and response to the Client Session identified by the verified Session ID. |
|
||||
| **FR-6** | The final Client Session leaving a Domain shall trigger teardown of transient Domain resources. |
|
||||
| **FR-7** | Startup and teardown shall recover safely from interrupted or stale prior executions. |
|
||||
| **FR-8** | Lazaret shall support interactive terminal sessions without degrading supported host terminal capabilities. |
|
||||
|
||||
### 4.3 Developer-Experience Requirements
|
||||
|
||||
| ID | Requirement |
|
||||
| :--- | :--- |
|
||||
| **DX-1** | A developer shall be able to launch an Agent Profile in the current workspace with one command. |
|
||||
| **DX-2** | Tools installed in configured persistent sandbox paths shall remain available on later launches of the same Domain. |
|
||||
| **DX-3** | Normal source editing, `git status`, staging, committing, and reference updates shall work from inside the Agent Sandbox. |
|
||||
| **DX-4** | Escalation prompts shall identify the requesting profile, session, command, and justification without mixing terminal input or output between sessions. |
|
||||
| **DX-5** | Failures shall report actionable diagnostics and shall not leave the developer's terminal in a corrupted mode. |
|
||||
|
||||
### 4.4 Operational Requirements
|
||||
|
||||
| ID | Requirement |
|
||||
| :--- | :--- |
|
||||
| **OR-1** | The coordinator shall maintain exclusive runtime ownership of Domain resources. |
|
||||
| **OR-2** | Every protocol request shall resolve to a completed, denied, timed-out, cancelled, or failed state. |
|
||||
| **OR-3** | Cancellation and process termination shall not strand an active host command or permanently block another Client Session. |
|
||||
| **OR-4** | Security-sensitive behavior shall be covered by integration tests on supported Podman, runsc, filesystem, and network configurations. Test evidence belongs in the journal. |
|
||||
|
|
|
|||
Loading…
Reference in a new issue