OOB SSH¶
Four ways write the same runtime directory, <project>/.eve-iac/oob/, for the authenticated agent session: the CLI, and the Python, TypeScript, and Go helpers. The generated OpenAPI client only performs the HTTP calls. The helper writes ssh_config, the user key, known_hosts, session_token, and agent_context, and it fills ProxyCommand with that runtime’s proxy.
No SDK implements SSH. No SDK shells out to another SDK. The CLI is the thin wrapper of the Python helper. Shell usage after the files exist: OOB SSH.
After materialize, from a process that does not have EVE_IAC_URL or EVE_IAC_TOKEN set:
ssh -F <project>/.eve-iac/oob/ssh_config <alias>
<alias> is the folded node name (iol1). HostName in the file is the canonical key (n_1). Do not append a remote command. exec is rejected.
start is idempotent while this session and this project stay active. stop revokes the generation. It deletes the directory only when the agent confirms the revoke or rejects the stored bearer. An unreachable agent leaves the files in place.
CLI¶
eve-iac oob start calls materialize_oob. The ProxyCommand it writes is python -m eveiac.oob.proxy. You do not run eve-iac oob proxy yourself; OpenSSH does, with --project and --node only.
export EVE_IAC_URL=https://eve.example:8787
export EVE_IAC_CA_FILE="$PWD/ca.pem"
export EVE_IAC_USERNAME=admin
export EVE_IAC_PASSWORD=...
eve-iac login
eve-iac oob start ./IaC/sample
eve-iac oob status ./IaC/sample
unset EVE_IAC_URL EVE_IAC_TOKEN
ssh -F ./IaC/sample/.eve-iac/oob/ssh_config iol1
oob start prints {active: true, ssh_config: <path>} and does not print the token. --json prints that object on stdout.
eve-iac oob start ./IaC/sample --json
eve-iac oob stop ./IaC/sample
oob stop does not take --token. It reads session_token from the project directory. oob credentials prints the private key; do not log it. --lab overrides eve.lab when the marker does not carry the EVE path.
Flag reference: eve-iac oob.
Python¶
materialize_oob derives the ProxyCommand as "<absolute python>" -m eveiac.oob.proxy. That interpreter must be able to import eveiac when OpenSSH runs the proxy.
import os
from eveiac import EveIacClient
from eveiac.oob import materialize_oob, stop_oob
url = os.environ["EVE_IAC_URL"]
ca = open(os.environ["EVE_IAC_CA_FILE"], encoding="utf-8").read()
project = "IaC/sample"
anon = EveIacClient(url=url, ca_pem=ca)
session = anon.login({
"username": os.environ["EVE_IAC_USERNAME"],
"password": os.environ["EVE_IAC_PASSWORD"],
})
client = EveIacClient(url=url, token=session.token, ca_pem=ca)
paths = materialize_oob(client, project, session.token, url, ca)
print(paths["ssh_config"])
# later, with the same files still on disk
stop_oob(project)
paths also contains id_ed25519, known_hosts, session_token, agent_context, and dir. Pass tls_pin="sha256:..." instead of ca when you pin the leaf and do not have the CA PEM. Pass entrypoint=[...] only when this process must not be the proxy; the default is the Python module.
Generated calls, if you are not using the helper: client.start_oob, client.get_oob_credentials, client.open_oob, client.stream_oob(ticket), client.stop_oob. The helper is what writes the files.
TypeScript¶
materializeOob derives the ProxyCommand from process.execPath and proxy.js next to the compiled helper (typescriptProxyEntrypoint()).
import * as fs from "node:fs";
import { createEveIacClient, materializeOob, stopOob } from "@eve-iac/sdk";
const url = process.env.EVE_IAC_URL!;
const caPem = fs.readFileSync(process.env.EVE_IAC_CA_FILE!, "utf8");
const projectPath = "IaC/sample";
const anon = createEveIacClient({ url, caPem });
const session = await anon.login({
username: process.env.EVE_IAC_USERNAME!,
password: process.env.EVE_IAC_PASSWORD!,
});
const client = createEveIacClient({ url, token: session.token, caPem });
const body: {
lab: string;
manifest: string;
yaml: string;
state?: unknown;
} = {
lab: "/IaC/sample.unl",
manifest: fs.readFileSync(`${projectPath}/.eve-iac.yml`, "utf8"),
yaml: fs.readFileSync(`${projectPath}/topology.yml`, "utf8"),
};
const statePath = `${projectPath}/.eve-iac-state.json`;
if (fs.existsSync(statePath)) {
body.state = JSON.parse(fs.readFileSync(statePath, "utf8"));
}
const paths = await materializeOob({
client,
projectPath,
token: session.token!,
origin: url,
caPem,
body,
});
console.log(paths.ssh_config);
await stopOob({ projectPath, body });
stopOob without client rebuilds the client from agent_context and session_token. Pass entrypoint only to replace typescriptProxyEntrypoint(). Pass tlsPin when there is no CA PEM.
The VSIX Prepare OOB SSH command calls this helper. In the extension host, process.execPath may be Electron; the extension then uses node plus the compiled proxy.js.
Go¶
Go cannot discover an installed binary. Build the proxy once, and pass only that path. MaterializeOOB appends --project and --node.
cd sdk/go
go build -o "$HOME/bin/eve-iac-oob-proxy" ./cmd/oob-proxy
package main
import (
"context"
"os"
"github.com/eve-iac/eve-iac/sdk/go/eveiac"
)
func main() {
ctx := context.Background()
url := os.Getenv("EVE_IAC_URL")
ca, err := os.ReadFile(os.Getenv("EVE_IAC_CA_FILE"))
if err != nil {
panic(err)
}
anon, err := eveiac.NewClient(eveiac.Config{URL: url, CAPEM: string(ca)})
if err != nil {
panic(err)
}
session, err := anon.Login(ctx, eveiac.LoginRequest{
Username: os.Getenv("EVE_IAC_USERNAME"),
Password: os.Getenv("EVE_IAC_PASSWORD"),
})
if err != nil {
panic(err)
}
client, err := eveiac.NewClient(eveiac.Config{
URL: url,
Token: *session.Token,
CAPEM: string(ca),
})
if err != nil {
panic(err)
}
project := "IaC/sample"
manifest, err := os.ReadFile(project + "/.eve-iac.yml")
if err != nil {
panic(err)
}
topology, err := os.ReadFile(project + "/topology.yml")
if err != nil {
panic(err)
}
body := eveiac.ProjectPayload{
Lab: eveiac.Ptr("/IaC/sample.unl"),
Manifest: eveiac.Ptr(string(manifest)),
YAML: eveiac.Ptr(string(topology)),
}
paths, err := eveiac.MaterializeOOB(
ctx,
client,
project,
*session.Token,
eveiac.AgentContext{Origin: url, CAPEM: string(ca)},
eveiac.ProxyCommand(os.Getenv("HOME")+"/bin/eve-iac-oob-proxy"),
body,
)
if err != nil {
panic(err)
}
println(paths["ssh_config"])
if err := eveiac.StopOOB(ctx, project, body); err != nil {
panic(err)
}
}
AgentContext.TLSPin (sha256:...) replaces CAPEM when you do not have the CA file. The ssh_config names eve-iac-oob-proxy only because that is the executable you passed. It does not embed a token.
Generated calls on the same client: StartOob, GetOobCredentials, OpenOob, StreamOob, StopOob. Those do not write the project directory.