fix(web): setup script step 2 checks pwd, no auto-mkdir (#344)

The /home onboarding page already has a visible manual "Step 3 — create
your workspace folder" instructing the user to `mkdir -p ~/<dir> && cd
~/<dir>` BEFORE pasting the install script into Claude Code. The pasted
script's step 2 then re-ran the same mkdir+cd, which silently overrode
an intentional alternate install path (e.g. user cd'd to
~/work/agnes-prod on purpose) and was redundant on the default path.

Step 2 now verifies the user is in `$HOME/<workspace_dir>` via `pwd`.
On mismatch it stops and asks the user to either re-paste from the
correct folder or reply `install here` to accept the current cwd.
Never auto-creates a folder.

Step 9 (restart Claude Code) references the install directory confirmed
in step 2 instead of a hardcoded `~/<workspace_dir>`, so users on a
custom path see accurate guidance.
This commit is contained in:
Vojtech 2026-05-19 13:20:33 +04:00 committed by GitHub
parent c6c72b9c00
commit bd90485dbd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 152 additions and 17 deletions

View file

@ -11,6 +11,15 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C
## [Unreleased] ## [Unreleased]
### Changed ### Changed
- Setup script no longer auto-creates the workspace folder. Step 2 of
the pasted prompt now runs `pwd`, compares it to `$HOME/<workspace_dir>`
(the folder the /home page's visible Step 3 told the user to create
manually), and on mismatch warns + asks the user to either re-paste
from the right folder or reply `install here` to accept the current
cwd. Respects an intentional alternate install path instead of
silently switching the user back to the default. Step 9 (restart
Claude Code) now references the install directory confirmed in step 2
rather than a hardcoded `~/<workspace_dir>`.
- **BREAKING (marketplace identifier)**: synthetic plugin bundling flea - **BREAKING (marketplace identifier)**: synthetic plugin bundling flea
skills + agents renamed from `agnes-store-bundle` to `flea`. The skills + agents renamed from `agnes-store-bundle` to `flea`. The
served `marketplace.json` now lists `flea` (previously served `marketplace.json` now lists `flea` (previously

View file

@ -324,19 +324,29 @@ def _install_cli_lines(*, has_ca: bool, server_url_placeholder: str = "{server_u
def _init_lines(server_url_placeholder: str = "{server_url}") -> list[str]: def _init_lines(server_url_placeholder: str = "{server_url}") -> list[str]:
"""Steps 2-4 — workspace folder bootstrap, then `agnes init` + smoke verify. """Steps 2-4 — workspace folder check, then `agnes init` + smoke verify.
Step 2 (new) explicitly creates the workspace folder and cd's into it. Step 2 verifies the user is already cd'd into the workspace folder
Previously the script assumed Claude Code was already cd'd to a sensible that the /home onboarding page's visible "Step 3 — create your
location and ran `agnes init --workspace .` against whatever that workspace folder" told them to create manually (`mkdir -p
happened to be. With the new explicit mkdir+cd the workspace path is ~/{workspace_dir} && cd ~/{workspace_dir}`). The pasted script
deterministic `~/{workspace_dir}` and the visible /home step block DOES NOT auto-create the folder that would silently override an
+ this scripted step stay in lockstep. intentional choice to install at a different path (e.g. the user
cd'd to `~/work/agnes-prod` on purpose). Instead we `pwd`, compare
to `$HOME/{workspace_dir}`, and on mismatch warn loudly and ask the
user to either re-paste from the right folder or explicitly confirm
"install here" in the current cwd.
`{workspace_dir}` and `{instance_brand}` are placeholders pre-substituted `{workspace_dir}` and `{instance_brand}` are placeholders pre-substituted
by :func:`resolve_lines` from the operator-configured brand. Defaults by :func:`resolve_lines` from the operator-configured brand. Defaults
keep `~/Agnes` behavior for instances that don't set the brand knob. keep `~/Agnes` behavior for instances that don't set the brand knob.
`agnes init --workspace .` continues to use the current cwd, so once
step 2 has confirmed (or the user has explicitly accepted) the
install dir, step 3 lands in the right place. Step 9's restart-claude
cue references "this same directory" so users on a custom path see
accurate guidance.
`agnes init` is the workspace-rails delivery mechanism for everyone: `agnes init` is the workspace-rails delivery mechanism for everyone:
it authenticates with the PAT, fetches CLAUDE.md (RBAC-filtered), it authenticates with the PAT, fetches CLAUDE.md (RBAC-filtered),
writes AGNES_WORKSPACE.md (human-facing docs), installs Claude Code writes AGNES_WORKSPACE.md (human-facing docs), installs Claude Code
@ -352,14 +362,36 @@ def _init_lines(server_url_placeholder: str = "{server_url}") -> list[str]:
""" """
return [ return [
"", "",
"2) Create the workspace folder in the user's home directory and cd into it:", "2) Verify the user is already in the workspace folder.",
" # POSIX (macOS / Linux / WSL):", " The /home page's visible \"Step 3 — create your workspace folder\"",
" mkdir -p \"$HOME/{workspace_dir}\" && cd \"$HOME/{workspace_dir}\"", " already asked the user to run",
" # Windows (PowerShell) — only run if the user is on Windows:", " mkdir -p ~/{workspace_dir} && cd ~/{workspace_dir}",
" # New-Item -ItemType Directory -Force -Path \"$HOME\\{workspace_dir}\" | Out-Null", " in their terminal BEFORE pasting this script. Do NOT silently",
" # Set-Location \"$HOME\\{workspace_dir}\"", " re-create the folder here — the user may have intentionally cd'd",
" to a different location to install {instance_brand} there instead.",
"", "",
" The remaining steps run inside this directory.", " Run:",
" pwd",
" Expected: $HOME/{workspace_dir} (i.e. ~/{workspace_dir})",
"",
" If `pwd` matches the expected path: continue silently to step 3.",
"",
" If `pwd` does NOT match, STOP and tell the user verbatim:",
"",
" \"You are in <current-pwd>, but {instance_brand} is normally",
" installed in ~/{workspace_dir} (see /home Step 3). Either run",
" mkdir -p ~/{workspace_dir} && cd ~/{workspace_dir}",
" in your terminal now and re-paste this setup script, OR reply",
" 'install here' to install {instance_brand} in <current-pwd>",
" instead. Reply 'abort' to stop.\"",
"",
" Wait for the user's reply.",
" - 'install here' → continue to step 3 in the current cwd.",
" The cwd you saw from `pwd` is the install",
" directory; remember it for step 9.",
" - 'abort' / anything else → stop without making any changes.",
" Do NOT run `mkdir`, do NOT `cd`, do NOT",
" continue to step 3.",
"", "",
"3) Bootstrap your {instance_brand} workspace in this directory:", "3) Bootstrap your {instance_brand} workspace in this directory:",
f" agnes init --server-url \"{server_url_placeholder}\" --token \"{{token}}\" --workspace .", f" agnes init --server-url \"{server_url_placeholder}\" --token \"{{token}}\" --workspace .",
@ -490,7 +522,7 @@ def _restart_claude_lines(step_num: str) -> list[str]:
return [ return [
"", "",
f"{step_num}) Restart Claude Code so every plugin, MCP server, and SessionStart hook installed above actually loads:", f"{step_num}) Restart Claude Code so every plugin, MCP server, and SessionStart hook installed above actually loads:",
" Tell me to type `/exit` (or close the Claude Code session entirely), then run `claude` again from this same `~/{workspace_dir}` directory.", " Tell me to type `/exit` (or close the Claude Code session entirely), then run `claude` again from this same directory — the install dir confirmed in step 2 (`~/{workspace_dir}` on the default path, or whatever cwd the user explicitly accepted with 'install here').",
" The next session boots with all marketplace plugins, every connector's keychain entries / OAuth grants, and the agnes-welcome + refresh-marketplace SessionStart hooks active. This is the last action before the Confirm summary — once I'm back in Claude Code, setup is complete.", " The next session boots with all marketplace plugins, every connector's keychain entries / OAuth grants, and the agnes-welcome + refresh-marketplace SessionStart hooks active. This is the last action before the Confirm summary — once I'm back in Claude Code, setup is complete.",
] ]

View file

@ -135,7 +135,10 @@ class TestInstanceBrand:
workspace_dir="FoundryAI", workspace_dir="FoundryAI",
)) ))
assert "Set up the Foundry AI CLI on this machine." in joined assert "Set up the Foundry AI CLI on this machine." in joined
assert "mkdir -p \"$HOME/FoundryAI\"" in joined # Step 2 is a pwd-check now (no auto-mkdir); brand + workspace_dir
# thread through the warning copy + expected-path string.
assert "$HOME/FoundryAI" in joined
assert "mkdir -p ~/FoundryAI && cd ~/FoundryAI" in joined
assert "Bootstrap your Foundry AI workspace" in joined assert "Bootstrap your Foundry AI workspace" in joined
assert "Foundry AI workspace is ready" in joined assert "Foundry AI workspace is ready" in joined
# No raw placeholders survive substitution. # No raw placeholders survive substitution.
@ -150,7 +153,10 @@ class TestInstanceBrand:
from app.web.setup_instructions import resolve_lines from app.web.setup_instructions import resolve_lines
joined = "\n".join(resolve_lines("agnes.whl")) joined = "\n".join(resolve_lines("agnes.whl"))
assert "Set up the Agnes CLI on this machine." in joined assert "Set up the Agnes CLI on this machine." in joined
assert "mkdir -p \"$HOME/Agnes\"" in joined # Step 2 is a pwd-check (no auto-mkdir); default path threads
# through as `$HOME/Agnes` + the warning's manual-mkdir example.
assert "$HOME/Agnes" in joined
assert "mkdir -p ~/Agnes && cd ~/Agnes" in joined
assert "Bootstrap your Agnes workspace" in joined assert "Bootstrap your Agnes workspace" in joined
assert "Agnes workspace is ready" in joined assert "Agnes workspace is ready" in joined
mod._instance_config = None mod._instance_config = None

View file

@ -1000,3 +1000,91 @@ def test_gws_prompt_emits_pass_fail_contract():
body = gws_prompt(gws_oauth_configured=False) body = gws_prompt(gws_oauth_configured=False)
assert "✅ Google Workspace ready" in body assert "✅ Google Workspace ready" in body
assert "❌ Google Workspace setup failed" in body assert "❌ Google Workspace setup failed" in body
# ---------------------------------------------------------------------------
# Step 2 — workspace folder check (no auto-mkdir).
# ---------------------------------------------------------------------------
def test_step_2_checks_pwd_does_not_auto_mkdir():
"""Step 2 must verify the user is already in the workspace folder
(the /home page Step 3 manual mkdir+cd) instead of silently re-running
`mkdir -p "$HOME/<dir>" && cd ` which would override an intentional
alternate install path.
Contract:
- Step 2 runs `pwd` and compares against `$HOME/<workspace_dir>`.
- Emits a warning message naming both the expected path and the
manual mkdir line the user already saw on /home.
- Does NOT emit `mkdir -p "$HOME/<workspace_dir>"` or the equivalent
PowerShell `New-Item -ItemType Directory -Force` line.
- Asks the user to reply 'install here' to proceed in a non-default
cwd, or 'abort' to stop. No automatic folder creation.
"""
from app.web.setup_instructions import resolve_lines
joined = "\n".join(resolve_lines("agnes.whl"))
# The new check-only step header.
assert "2) Verify the user is already in the workspace folder." in joined
# `pwd` check + expected-path comparison must be present.
assert "pwd" in joined
assert "$HOME/Agnes" in joined # default workspace_dir
assert "~/Agnes" in joined
# Warning copy that references the /home Step 3 manual mkdir line.
assert "/home Step 3" in joined
assert "mkdir -p ~/Agnes && cd ~/Agnes" in joined
# Explicit "install here" / "abort" decision tree.
assert "'install here'" in joined
assert "'abort'" in joined
# The auto-mkdir lines from the previous step 2 must be GONE.
assert 'mkdir -p "$HOME/Agnes"' not in joined
assert 'mkdir -p "$HOME/{workspace_dir}"' not in joined
assert 'New-Item -ItemType Directory -Force -Path "$HOME\\Agnes"' not in joined
assert "Set-Location \"$HOME\\Agnes\"" not in joined
def test_step_2_warning_substitutes_custom_brand():
"""Custom brand + workspace_dir must thread through the new step 2
warning copy no leftover placeholders, expected path matches the
operator's configured workspace dir."""
from app.web.setup_instructions import resolve_lines
joined = "\n".join(resolve_lines(
"agnes.whl",
instance_brand="Foundry AI",
workspace_dir="FoundryAI",
))
assert "2) Verify the user is already in the workspace folder." in joined
assert "$HOME/FoundryAI" in joined
assert "~/FoundryAI" in joined
assert "mkdir -p ~/FoundryAI && cd ~/FoundryAI" in joined
# Brand + workspace_dir thread through the warning copy (the text
# wraps across two lines so we check the substrings separately).
assert "but Foundry AI is normally" in joined
assert "installed in ~/FoundryAI" in joined
# No placeholders survive into the rendered text.
assert "{workspace_dir}" not in joined
assert "{instance_brand}" not in joined
def test_step_9_restart_references_install_dir_not_hardcoded():
"""Step 9 must describe the restart cwd as the directory confirmed in
step 2 (mentioning the default path) rather than a bare hardcoded
`~/{workspace_dir}`. This keeps the wording accurate when the user
chose an alternate install path via 'install here' in step 2."""
from app.web.setup_instructions import resolve_lines
joined = "\n".join(resolve_lines("agnes.whl"))
assert "9) Restart Claude Code" in joined
# Wording references the step-2 confirmation.
assert "install dir confirmed in step 2" in joined
# Default path still mentioned as the expected baseline.
assert "~/Agnes" in joined
# The "install here" callout in the restart step keeps the user-flow
# connection visible.
assert "'install here'" in joined