Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -8286,8 +8286,8 @@ browser would move a private key off the person's computer. Trust on first use w
let the first answer through an unknown network choose the host key. Opening the
network grant from the Launchpad would take the Admin's decision.

**Known limits and open questions.** Two Organizations whose Environments share a node
name give the same alias; the prompt's rule makes the agent stop instead of overwriting.
**Known limits and open questions.** Legacy unbound Environments use the node label
as their alias. The binding-based alias below separates hosted Environment identities.
On a computer with several tailnet profiles Tailscale names the node in the active one.
**Team Environments:** everyone admitted there acts as the Team's account, so any of them
may add or remove any device key; the page shows who added each and that the devices are
Expand All @@ -8296,6 +8296,45 @@ rule (per-person accounts, or approval of each device key) is an open question f
Operator. Consumer acceptance on a Remote Environment, with a real device, its network
grant and `ssh <alias> true`, is still to be recorded.


### F49 clarification — device-to-Codex handoff (2026-10-12)

A review of two work Environments of the same Operator found independently generated
laptop prompts using the same `Host` alias. Retargeting that alias could replace the
first connection; a leftover `HostKeyAlias` could also select the first host's pin.
The Platform now derives a work alias as `lazurio.org.<organization>.<machine>` and a
personal alias as `lazurio.personal.<owner>.<machine>` from the validated Machine
binding. Dot-separated identity components avoid the ambiguity of concatenating
hyphenated names. Host, key path, HostKeyAlias and the known_hosts pin share that
alias. An unbound installation retains the original node-label convention.

`GET /api/ssh` also returns the actual canonical Folder path and a Codex link. The
page owns these values; agents copy the recipe rather than infer identity, paths or
URI parameters. The concise generated manual explains which device does which part,
where the recipe lives, and the separate Admin-owned network grant. It delegates
ordinary SSH setup and diagnosis to the laptop agent. The link carries `name` (the
OpenSSH alias), `projectPath` (the canonical remote Folder) and `enabled=true`, with
URLSearchParams encoding and no credentials. The page validates these fields before
rendering the link. A client without this capability uses Settings → Connections → SSH.

The change does not rewrite a laptop's existing aliases, revoke keys or move Codex
history. A new binding-based alias may create a separate desktop connection from an
old alias; the operator can retire the old connection after verifying the new one.
The link registers a host and creates/reuses its project only once SSH works. It does
not grant network admission or replace the laptop's private key. Desktop completion
requires a real client acceptance check; source inspection is not that evidence.

**Capability evidence.** OpenAI's [remote connections documentation](https://learn.chatgpt.com/docs/remote-connections)
confirms concrete OpenSSH aliases, remote Codex availability and remote-folder selection.
Read-only inspection of installed ChatGPT Desktop 26.1007.30912 (build 20156) on
2026-10-12 confirms `codex://settings/connections/ssh/add`, its `name`, `projectPath`
and `enabled` query parameters, SSH registration by alias, the absolute-path check and
remote-project creation with `reuseExisting: true`. The `alias` query spelling is a
synonym for `name`, not an independent display-name parameter; do not send both.
There is no `projectName` parameter. This is implementation evidence for that build,
not a published stable URI schema or a completed laptop acceptance test. Source
inspection and rendered guidance do not prove successful registration on another device.

## F50 — The Environment browser's contract: one module, executable vectors and an attested artifact for the forks

**Decided by Matěj 2026-10-12 on the agent's proposal (root decision 0191's addendum of
Expand Down
31 changes: 31 additions & 0 deletions src/folder/manual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1840,6 +1840,36 @@ function enabledToolsSection(
];
}

// F49: Launchpad owns SSH enrollment and supplies the laptop handoff.
// The desktop link registers a client connection, never a network grant.
function deviceSsh(machine: MachineBinding): readonly Text[] {
const origin = machine.entry?.externalOrigin;
return [
blank,
t(
"## Připojení počítače a projektu v Codexu",
"## Connecting a computer and a Codex project",
),
t(
"Připojení nastavuje agent na Operátorově počítači, ne ty. Jsi v Environmentu, ke kterému se počítač připojuje, a soukromý klíč počítače sem nepatří.",
"The agent on the Operator's computer sets up the connection, not you. You are in the Environment the computer connects to, and the computer's private key does not belong here.",
),
origin === undefined
? t(
"Handover neuvádí adresu Launchpadu, tak ji neodvozuj. Požádej správce Environmentu, ať ji doplní.",
"The handover gives no Launchpad address, so do not guess it. Ask the Environment's administrator to add it.",
)
: t(
`Pošli Operátorovi odkaz na [Nastavení → SSH ze zařízení](${origin}/settings/ssh). Čeká tam hotový prompt pro agenta na jeho počítači: jednoznačný alias tohoto Environmentu, klíč hostu a odkaz, který přidá tento Lazurio Folder do Codexu jako projekt. Údaje ber z té stránky.`,
`Send the Operator the link to [Settings → SSH from your devices](${origin}/settings/ssh). A prompt for the agent on their computer is ready there: a distinct alias for this Environment, its host key and a link that adds this Lazurio Folder to Codex as a project. Take the values from that page.`,
),
t(
"Zaregistrovaný klíč sám přístup do sítě nedává. Která zařízení se do sítě dostanou, rozhoduje správce sítě (Headscale a povolení SSH). Profil Tailscale na Operátorově počítači nepřepínej.",
"A registered key gives no network access by itself. The network administrator decides which devices reach the network (Headscale and the SSH grant). Do not switch the Tailscale profile on the Operator's computer.",
),
];
}

// How the Operator works with an agent on a hosted Environment (decision F14
// addendum 2026-10-02). The Operator uses the clients they prefer, possibly
// several at once: ChatGPT Desktop over SSH, T3 Code on the web or in its
Expand Down Expand Up @@ -2216,6 +2246,7 @@ function thisMachine(source: InstructionSource): string {
"Lazurio enforces none of this: the Headscale policy of the Conglomerate Host does, and transport is not authentication, so SSH keys and application sign-in stay separate.",
),
]),
...(machine === null ? [] : deviceSsh(machine)),
...(machine === null ? [] : [blank, ...sshRules]),
...(preset === "hosted-personal"
? [blank, ...fromPersonalVm(machine, locale)]
Expand Down
6 changes: 5 additions & 1 deletion src/folder/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import { ownDataValue, stateFields } from "./state-fields";
// future persisted preference schemas. Every change of the generated bytes
// moves it to the next number; tests/folder-template-revision.test.ts pins the
// rendered digest of the current one (decision F14, addendum 2026-10-03).
export const instructionTemplateRevision = "base-instructions-45";
export const instructionTemplateRevision = "base-instructions-46";

// Template revisions are ordered by their number. A Folder rendered by an
// older revision is re-rendered by the next change of the generated Folder
Expand Down Expand Up @@ -473,6 +473,10 @@ function hostedLines(
const ssh = operatorConnectsOverSsh(machine);
const origin = machine.entry?.externalOrigin;
return [
pick({
cs: "- Když Operátor chce připojit svůj laptop přes SSH nebo připravit prompt pro Codex Desktop na laptopu, nejdřív čti oddíl Připojení laptopu a projektu Codex v `manual/this-machine.md`.",
en: "- When the Operator wants to connect their laptop over SSH or asks for a prompt for Codex Desktop on the laptop, first read Connecting a laptop and a Codex project in `manual/this-machine.md`.",
}),
pick({
cs: "- SSH na jiný Environment nebo zařízení jen na jeho tailnet hostname, s pinnutým host klíčem a po ověření aktivního tailnetu, nikdy na holou adresu `100.64.0.x` (`manual/this-machine.md`).",
en: "- SSH to another Environment or device only to its tailnet hostname, with a pinned host key and after verifying the active tailnet, never to a bare `100.64.0.x` address (`manual/this-machine.md`).",
Expand Down
3 changes: 2 additions & 1 deletion src/launchpad/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1014,11 +1014,12 @@ <h2 id="update-heading" class="sr-only" data-message="updateTitle">Product updat
<div class="group-head"><h2 id="ssh-computer-heading" data-message="sshComputerTitle">Settings for your computer</h2><p class="group-note" data-message="sshComputerNote">Your agent writes these on your computer. The host key is this Environment's own, so your computer checks that it really talks to this Environment.</p></div>
<p id="ssh-added" class="callout" role="status" aria-live="polite" hidden></p>
<div class="card" id="ssh-computer-ready" hidden>
<div class="row"><div><a id="ssh-codex" data-message="sshCodexLink" hidden>Add Environment to Codex</a><p id="ssh-project-path" class="row-desc"></p></div></div>
<div class="row ssh-block-row"><div class="ssh-block-head"><p class="row-title" data-message="sshConfigLabel">SSH config</p><p class="row-desc" data-message="sshConfigNote">Add to ~/.ssh/config</p></div><pre id="ssh-config" class="ssh-block" data-ssh-block="config"></pre><div class="ssh-block-actions"><span class="row-status" data-copy-status role="status" aria-live="polite"></span><button type="button" data-copy="ssh-config"><svg class="icon" aria-hidden="true"><use href="#i-copy"/></svg><span data-message="sshCopy">Copy</span></button></div></div>
<div class="row ssh-block-row"><div class="ssh-block-head"><p class="row-title" data-message="sshKnownHostsLabel">known_hosts</p><p class="row-desc" data-message="sshKnownHostsNote">Add to ~/.ssh/known_hosts_lazurio</p></div><pre id="ssh-known-hosts" class="ssh-block" data-ssh-block="known-hosts"></pre><div class="ssh-block-actions"><span class="row-status" data-copy-status role="status" aria-live="polite"></span><button type="button" data-copy="ssh-known-hosts"><svg class="icon" aria-hidden="true"><use href="#i-copy"/></svg><span data-message="sshCopy">Copy</span></button></div></div>
</div>
<div id="ssh-computer-missing" class="callout" data-kind="warning" role="note" hidden></div>
<p class="group-note" data-message="sshNetworkNote">If the connection is refused or times out, the network does not let that device reach this Environment over SSH yet: ask your Organization's Admin to allow SSH from that device to this Environment.</p>
<p class="group-note" data-message="sshNetworkNote">Adding a key does not give this device network access by itself. If SSH fails, give your agent the exact error. If the device is not allowed on the network yet, the network administrator can allow it.</p>
</section>
<section class="group" id="ssh-outside-group" aria-labelledby="ssh-outside-heading" hidden>
<div class="group-head"><h2 id="ssh-outside-heading" data-message="sshOutsideTitle">Added outside Launchpad</h2><p class="group-note" data-message="sshOutsideNote">Launchpad never changes or removes these keys, for example the key this Environment was set up with. Whoever looks after this Environment removes them.</p></div>
Expand Down
10 changes: 6 additions & 4 deletions src/launchpad/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1244,7 +1244,7 @@ const en = {
sshPromptUnavailable:
"The prompt appears once the settings for your computer are known (below).",
sshPromptText:
'Set up SSH from this computer to my Lazurio Environment "{alias}".\n\n1. Open {url} in your browser panel. If a sign-in appears, wait until I have signed in with my Lazurio account; never type my password yourself.\n2. On this computer, use the key {keyPath} (on Windows {windowsKeyPath}). If it does not exist yet, create it: ssh-keygen -t ed25519 -N "" -C {alias} -f {keyPath} (in Windows PowerShell 5.1 pass the empty passphrase as -N \'""\'; on Windows ~ is %USERPROFILE%)\n Never show, copy or send the private key anywhere; only the .pub file leaves this computer.\n3. On the page, under "Add a device", fill in this computer\'s name and the whole content of {keyPath}.pub, then press "Add device".\n4. From "Settings for your computer", add the "SSH config" block to ~/.ssh/config and the "known_hosts" line to ~/.ssh/known_hosts_lazurio (create the files if they are missing). If ~/.ssh/config already has a "Host {alias}" block, or known_hosts_lazurio another key for {alias}, change nothing and tell me.\n5. Check the connection: ssh {alias} true\n\nRules: never change, switch or log out a Tailscale profile. Never turn StrictHostKeyChecking off and never accept a host key other than the one on the page. If ssh is refused or times out on the network, this device is not allowed to reach the Environment over SSH yet: tell me to ask my Organization\'s Admin to allow SSH from this device to the Environment "{alias}". Finally, tell me what you did and the result of the check.',
"Connect this computer to my Lazurio Environment \"{alias}\" over SSH and add its Lazurio Folder to Codex as a project.\n\nThe Environment's page has its address, account and host key: {url}\n\n1. Use the key {keyPath} (on Windows {windowsKeyPath}). If it does not exist yet, create an ed25519 key. The private key never leaves this computer; only the .pub file does.\n2. On the page, under \"Add a device\", register its public key.\n3. Add the page's SSH settings and known_hosts line for {alias}. Leave other connections as they are. If the same entry already exists, use it; if a different one exists for {alias}, change nothing and tell me.\n4. Check that SSH works: ssh {alias} true\n5. Then open the page's Codex link: {desktopLink}. If the app cannot open it, add the alias and the page's Folder path yourself in Settings → Connections → SSH.\n\nDo not switch the Tailscale profile. A registered key does not give network access; the network administrator allows that. Finally, tell me how the connection went.",
sshDevicesTitle: "Your devices",
sshDevicesNone: "No device has been added here yet.",
sshAddedBy: "Added {date} by {who}",
Expand Down Expand Up @@ -1308,6 +1308,7 @@ const en = {
sshRefusedName: "Give the device a name with letters or digits.",
sshRefusedAddedBy:
"The signed-in account could not be recorded. Reload the page and try again.",
sshCodexLink: "Add Environment to Codex",
sshComputerTitle: "Settings for your computer",
sshComputerNote:
"Your agent writes these on your computer. The host key is this Environment's own, so your computer checks that it really talks to this Environment.",
Expand All @@ -1318,7 +1319,7 @@ const en = {
sshCopy: "Copy",
sshCopied: "Copied.",
sshNetworkNote:
"If the connection is refused or times out, the network does not let that device reach this Environment over SSH yet: ask your Organization's Admin to allow SSH from that device to this Environment.",
"Adding a key does not give this device network access by itself. If SSH fails, give your agent the exact error. If the device is not allowed on the network yet, the network administrator can allow it.",
sshComputerMissing: "The settings for your computer are not available yet:",
sshMissingTailscale: "Tailscale is not installed in this Environment.",
sshMissingTailscaleFailed: "Tailscale in this Environment did not answer.",
Expand Down Expand Up @@ -2550,7 +2551,7 @@ const cs: Record<MessageKey, string> = {
sshPromptUnavailable:
"Prompt se objeví, jakmile budou známá nastavení pro váš počítač (níže).",
sshPromptText:
'Nastav SSH z tohoto počítače do mého Lazurio Environmentu „{alias}“.\n\n1. Otevři {url} ve svém panelu prohlížeče. Když se objeví přihlášení, počkej, až se přihlásím svým účtem Lazurio; moje heslo nikdy nezadávej sám.\n2. Na tomhle počítači použij klíč {keyPath} (ve Windows {windowsKeyPath}). Když ještě neexistuje, vytvoř ho: ssh-keygen -t ed25519 -N "" -C {alias} -f {keyPath} (ve Windows PowerShell 5.1 předej prázdné heslo jako -N \'""\'; ve Windows je ~ složka %USERPROFILE%)\n Soukromý klíč nikdy nikomu neukazuj, nekopíruj ani neposílej; z počítače smí odejít jen soubor .pub.\n3. Na stránce v části „Přidat zařízení“ vyplň název tohoto počítače a celý obsah souboru {keyPath}.pub a stiskni „Přidat zařízení“.\n4. Z části „Nastavení pro váš počítač“ přidej blok „SSH config“ do ~/.ssh/config a řádek „known_hosts“ do ~/.ssh/known_hosts_lazurio (chybějící soubory vytvoř). Když ~/.ssh/config už má blok „Host {alias}“ nebo known_hosts_lazurio jiný klíč pro {alias}, nic neměň a řekni mi to.\n5. Ověř spojení: ssh {alias} true\n\nPravidla: profil Tailscale nikdy neměň, nepřepínej ani neodhlašuj. StrictHostKeyChecking nikdy nevypínej a nepřijímej jiný klíč hostu než ten ze stránky. Když síť ssh odmítne nebo vyprší, tohle zařízení zatím do Environmentu přes SSH nesmí: řekni mi, ať požádám Admina své Organizace, aby povolil SSH z tohoto zařízení do Environmentu „{alias}“. Nakonec mi řekni, co jsi udělal, a výsledek ověření.',
"Připoj tento počítač přes SSH k mému Lazurio Environmentu „{alias}“ a přidej jeho Lazurio Folder do Codexu jako projekt.\n\nAdresu, účet a klíč hostu najdeš na stránce Environmentu: {url}\n\n1. Použij klíč {keyPath} (ve Windows {windowsKeyPath}). Když ještě neexistuje, vytvoř klíč ed25519. Soukromý klíč z počítače nikdy neodchází, ven smí jen soubor .pub.\n2. Na stránce v části „Přidat zařízení“ zaregistruj jeho veřejný klíč.\n3. Doplň nastavení SSH a řádek known_hosts ze stránky pro {alias}. Ostatní připojení nech, jak jsou. Když stejný záznam už existuje, použij ho; když pro {alias} existuje jiný, nic neměň a řekni mi to.\n4. Ověř, že SSH funguje: ssh {alias} true\n5. Pak otevři odkaz do Codexu ze stránky: {desktopLink}. Když ho aplikace neotevře, přidej alias a cestu k Folderu ze stránky sám v Settings → Connections → SSH.\n\nProfil Tailscale nepřepínej. Zaregistrovaný klíč přístup do sítě nedává, ten povoluje správce sítě. Nakonec mi napiš, jak připojení dopadlo.",
sshDevicesTitle: "Vaše zařízení",
sshDevicesNone: "Zatím tu není přidané žádné zařízení.",
sshAddedBy: "Přidáno {date}, účet {who}",
Expand Down Expand Up @@ -2614,6 +2615,7 @@ const cs: Record<MessageKey, string> = {
sshRefusedName: "Pojmenujte zařízení písmeny nebo číslicemi.",
sshRefusedAddedBy:
"Přihlášený účet se nepodařilo zaznamenat. Načtěte stránku znovu a zkuste to ještě jednou.",
sshCodexLink: "Přidat Environment do Codexu",
sshComputerTitle: "Nastavení pro váš počítač",
sshComputerNote:
"Váš agent je zapíše do vašeho počítače. Klíč hostu je vlastní klíč tohoto Environmentu, takže si počítač ověří, že opravdu mluví s ním.",
Expand All @@ -2624,7 +2626,7 @@ const cs: Record<MessageKey, string> = {
sshCopy: "Kopírovat",
sshCopied: "Zkopírováno.",
sshNetworkNote:
"Když spojení selže nebo vyprší, síť zatím nepouští to zařízení do tohoto Environmentu přes SSH: požádejte Admina své Organizace, aby SSH z toho zařízení do tohoto Environmentu povolil.",
"Přidaný klíč zařízení sám přístup do sítě nedá. Když SSH selže, dejte agentovi přesnou chybovou hlášku. Pokud zařízení do sítě zatím nesmí, povolí mu to správce sítě.",
sshComputerMissing: "Nastavení pro váš počítač zatím není k dispozici:",
sshMissingTailscale: "V tomhle Environmentu není nainstalovaný Tailscale.",
sshMissingTailscaleFailed: "Tailscale v tomhle Environmentu neodpověděl.",
Expand Down
Loading
Loading