From 9e3e611aabc8d89d243263fdb76e1ec52f7f1881 Mon Sep 17 00:00:00 2001 From: Vojtech Rysanek Date: Thu, 21 May 2026 18:17:25 +0400 Subject: [PATCH 1/2] fix(web): install-prompt Step 2 + restart cue match Desktop install path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /home page Step 2 told users to mkdir ~/Desktop/, but the pasted install script's Step 2 pwd-check expected $HOME/ and warned 'normally installed in ~/FoundryAI' — sending users on the Desktop path through an unnecessary 'install here' confirmation. Align Step 2 (pwd check + warning copy + manual mkdir hint), Step 9 restart-claude cue, post-install /home hero, and the 'don't create Projects/' callout to ~/Desktop/. Update tests. --- CHANGELOG.md | 7 ++++++ app/web/setup_instructions.py | 28 +++++++++++------------ app/web/templates/home_not_onboarded.html | 4 ++-- app/web/templates/home_onboarded.html | 2 +- tests/test_instance_config.py | 12 +++++----- tests/test_setup_instructions.py | 20 ++++++++-------- 6 files changed, 40 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6b3276..584c6a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,13 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C standard step-lede size instead of the previous 13px chip. ### Fixed +- Install-script Step 2 + Step 9 restart cue + post-install `/home` hero + now reference `~/Desktop/` to match the `/home` "Step 2 + — pick a folder" recommendation users actually run (`mkdir -p + ~/Desktop/`). Previously the pasted setup script + checked `pwd` against `$HOME/` and would warn + "Foundry AI is normally installed in ~/FoundryAI" even though the + /home page had just sent the user to `~/Desktop/FoundryAI`. - Pre-login pages (`/login`, magic-link screens, first-time `/setup`) now honour the configured `instance.theme`. `base_login.html` sets `` from `instance_theme`, additionally loads diff --git a/app/web/setup_instructions.py b/app/web/setup_instructions.py index 79c21c6..12d2363 100644 --- a/app/web/setup_instructions.py +++ b/app/web/setup_instructions.py @@ -337,19 +337,19 @@ def _init_lines(server_url_placeholder: str = "{server_url}") -> list[str]: """Steps 2-4 — workspace folder check, then `agnes init` + smoke verify. Step 2 verifies the user is already cd'd into the workspace folder - that the /home onboarding page's visible "Step 2 — create your - workspace folder" told them to create manually (`mkdir -p - ~/{workspace_dir} && cd ~/{workspace_dir}`). The pasted script - DOES NOT auto-create the folder — that would silently override an + that the /home onboarding page's visible "Step 2 — pick a folder" + told them to create manually (`mkdir -p ~/Desktop/{workspace_dir} + && cd ~/Desktop/{workspace_dir}`). The pasted script DOES NOT + auto-create the folder — that would silently override an 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. + to `$HOME/Desktop/{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 by :func:`resolve_lines` from the operator-configured brand. Defaults - keep `~/Agnes` behavior for instances that don't set the brand knob. + keep `~/Desktop/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 @@ -373,24 +373,24 @@ def _init_lines(server_url_placeholder: str = "{server_url}") -> list[str]: return [ "", "2) Verify the user is already in the workspace folder.", - " The /home page's visible \"Step 2 — create your workspace folder\"", + " The /home page's visible \"Step 2 — pick a folder\"", " already asked the user to run", - " mkdir -p ~/{workspace_dir} && cd ~/{workspace_dir}", + " mkdir -p ~/Desktop/{workspace_dir} && cd ~/Desktop/{workspace_dir}", " in their terminal BEFORE pasting this script. Do NOT silently", " re-create the folder here — the user may have intentionally cd'd", " to a different location to install {instance_brand} there instead.", "", " Run:", " pwd", - " Expected: $HOME/{workspace_dir} (i.e. ~/{workspace_dir})", + " Expected: $HOME/Desktop/{workspace_dir} (i.e. ~/Desktop/{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 , but {instance_brand} is normally", - " installed in ~/{workspace_dir} (see /home Step 2). Either run", - " mkdir -p ~/{workspace_dir} && cd ~/{workspace_dir}", + " installed in ~/Desktop/{workspace_dir} (see /home Step 2). Either run", + " mkdir -p ~/Desktop/{workspace_dir} && cd ~/Desktop/{workspace_dir}", " in your terminal now and re-paste this setup script, OR reply", " 'install here' to install {instance_brand} in ", " instead. Reply 'abort' to stop.\"", @@ -549,7 +549,7 @@ def _restart_claude_lines(step_num: str) -> list[str]: return [ "", 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 directory — the install dir confirmed in step 2 (`~/{workspace_dir}` on the default path, or whatever cwd the user explicitly accepted with 'install here').", + " 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 (`~/Desktop/{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.", ] diff --git a/app/web/templates/home_not_onboarded.html b/app/web/templates/home_not_onboarded.html index 765bc51..67f66e0 100644 --- a/app/web/templates/home_not_onboarded.html +++ b/app/web/templates/home_not_onboarded.html @@ -2485,7 +2485,7 @@ What can I help you with? {% endwith %}
- The preview above is the exact text the button copies; the placeholder is replaced with a real token at click time. Don't create ~/{{ workspace_dir }}/Projects/ manually — the bundled plugin offers to set it up after install. + The preview above is the exact text the button copies; the placeholder is replaced with a real token at click time. Don't create ~/Desktop/{{ workspace_dir }}/Projects/ manually — the bundled plugin offers to set it up after install.
@@ -2607,7 +2607,7 @@ What can I help you with? {% if onboarded %} {# Offboarding escape hatch shown only after the hero has disappeared. - Lets the analyst (e.g. after wiping ~/{{ workspace_dir }}) flip the + Lets the analyst (e.g. after wiping ~/Desktop/{{ workspace_dir }}) flip the users.onboarded boolean back to false so the full install hero renders again on next reload. Discrete by design — onboarded users land on /home expecting the nav hub, not a setup screen. #} diff --git a/app/web/templates/home_onboarded.html b/app/web/templates/home_onboarded.html index 69b57df..06f250a 100644 --- a/app/web/templates/home_onboarded.html +++ b/app/web/templates/home_onboarded.html @@ -114,7 +114,7 @@
Welcome back, {{ display_name }}

You're all set up

- Open Claude Code in any project under ~/{{ workspace_dir }}/Projects/ + Open Claude Code in any project under ~/Desktop/{{ workspace_dir }}/Projects/ and start a session — your data and plugins are already synced. Use the cards below to jump into the parts of {{ instance_brand }} you need.

diff --git a/tests/test_instance_config.py b/tests/test_instance_config.py index 4855692..246bccb 100644 --- a/tests/test_instance_config.py +++ b/tests/test_instance_config.py @@ -137,8 +137,8 @@ class TestInstanceBrand: assert "Set up the Foundry AI CLI on this machine." 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 "$HOME/Desktop/FoundryAI" in joined + assert "mkdir -p ~/Desktop/FoundryAI && cd ~/Desktop/FoundryAI" in joined assert "Bootstrap your Foundry AI workspace" in joined assert "Foundry AI workspace is ready" in joined # No raw placeholders survive substitution. @@ -148,15 +148,15 @@ class TestInstanceBrand: def test_default_brand_keeps_agnes_branding(self, tmp_path, monkeypatch): """Backwards-compat: callers that don't pass brand/workspace_dir - get the literal 'Agnes' / '~/Agnes' rendering.""" + get the literal 'Agnes' / '~/Desktop/Agnes' rendering.""" mod = self._reload(tmp_path, monkeypatch) from app.web.setup_instructions import resolve_lines joined = "\n".join(resolve_lines("agnes.whl")) assert "Set up the Agnes CLI on this machine." 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 + # through as `$HOME/Desktop/Agnes` + the warning's manual-mkdir example. + assert "$HOME/Desktop/Agnes" in joined + assert "mkdir -p ~/Desktop/Agnes && cd ~/Desktop/Agnes" in joined assert "Bootstrap your Agnes workspace" in joined assert "Agnes workspace is ready" in joined mod._instance_config = None diff --git a/tests/test_setup_instructions.py b/tests/test_setup_instructions.py index 4dded83..a5b35c1 100644 --- a/tests/test_setup_instructions.py +++ b/tests/test_setup_instructions.py @@ -921,7 +921,7 @@ def test_restart_claude_step_emitted_unconditionally(): def test_restart_claude_substitutes_workspace_dir(): """The restart-claude body interpolates the workspace folder so the - user sees their actual `~/` path, not a literal placeholder.""" + user sees their actual `~/Desktop/` path, not a literal placeholder.""" from app.web.setup_instructions import resolve_lines joined = "\n".join(resolve_lines( @@ -930,7 +930,7 @@ def test_restart_claude_substitutes_workspace_dir(): workspace_dir="FoundryAI", )) assert "9) Restart Claude Code" in joined - assert "~/FoundryAI" in joined + assert "~/Desktop/FoundryAI" in joined assert "{workspace_dir}" not in joined @@ -1030,12 +1030,12 @@ def test_step_2_checks_pwd_does_not_auto_mkdir(): # `pwd` check + expected-path comparison must be present. assert "pwd" in joined - assert "$HOME/Agnes" in joined # default workspace_dir - assert "~/Agnes" in joined + assert "$HOME/Desktop/Agnes" in joined # default workspace_dir under Desktop + assert "~/Desktop/Agnes" in joined # Warning copy that references the /home Step 2 manual mkdir line. assert "/home Step 2" in joined - assert "mkdir -p ~/Agnes && cd ~/Agnes" in joined + assert "mkdir -p ~/Desktop/Agnes && cd ~/Desktop/Agnes" in joined # Explicit "install here" / "abort" decision tree. assert "'install here'" in joined @@ -1060,13 +1060,13 @@ def test_step_2_warning_substitutes_custom_brand(): 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 + assert "$HOME/Desktop/FoundryAI" in joined + assert "~/Desktop/FoundryAI" in joined + assert "mkdir -p ~/Desktop/FoundryAI && cd ~/Desktop/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 + assert "installed in ~/Desktop/FoundryAI" in joined # No placeholders survive into the rendered text. assert "{workspace_dir}" not in joined assert "{instance_brand}" not in joined @@ -1084,7 +1084,7 @@ def test_step_9_restart_references_install_dir_not_hardcoded(): # 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 + assert "~/Desktop/Agnes" in joined # The "install here" callout in the restart step keeps the user-flow # connection visible. assert "'install here'" in joined From 487f8405966c5f45b1ed9964ae78c81e7a0fbed5 Mon Sep 17 00:00:00 2001 From: Vojtech Rysanek Date: Thu, 21 May 2026 18:21:33 +0400 Subject: [PATCH 2/2] =?UTF-8?q?fix(web):=20GWS=20verify=20step=20=E2=80=94?= =?UTF-8?q?=20drop=20fragile=20JSON-parse=20+=20add=20anti-footgun=20guida?= =?UTF-8?q?nce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 8 of the Google Workspace connector prompt told Claude to call `gws drive files list` + `gws chat spaces list` and parse counts out of the JSON response. In practice Claude improvised a `python3 -c 'f"… {len(d.get(\"files\",[]))}…"'` snippet that fails in two ways: f-string expressions reject backslashes pre-3.12 (SyntaxError), and gws can emit a banner ahead of the JSON body (JSONDecodeError on `json.load(sys.stdin)`). Treat exit code 0 from each gws call as success, drop the ` drive file(s), chat space(s) visible` counts (consistent with the Step 0 precheck), and explicitly warn off the two anti-patterns. Summary-grep prefix `✅ Google Workspace ready —` is preserved so the install-summary still picks the line up. --- CHANGELOG.md | 10 ++++++++++ app/web/connector_prompts.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 584c6a7..afcab69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,16 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C standard step-lede size instead of the previous 13px chip. ### Fixed +- Google Workspace connector prompt's Step 8 verify no longer asks + Claude to parse a row count out of `gws drive files list` / `gws + chat spaces list` JSON. Claude would improvise a `python3 -c 'f"… + {len(d.get(\"files\",[]))}…"'` snippet that fails two ways: f-string + expressions reject backslashes in Python <3.12 (`SyntaxError`), and + `gws` can emit a banner before the JSON body (`json.JSONDecodeError`). + Step 8 now treats exit code 0 as success, drops the ` drive + file(s), chat space(s) visible` counts, and explicitly warns + against both anti-patterns. The summary-grep prefix (`✅ Google + Workspace ready —`) is preserved. - Install-script Step 2 + Step 9 restart cue + post-install `/home` hero now reference `~/Desktop/` to match the `/home` "Step 2 — pick a folder" recommendation users actually run (`mkdir -p diff --git a/app/web/connector_prompts.py b/app/web/connector_prompts.py index 971388f..21e3232 100644 --- a/app/web/connector_prompts.py +++ b/app/web/connector_prompts.py @@ -343,7 +343,7 @@ _GWS_PROMPT_TAIL_TEMPLATE = """ 7. Find where gws stored my credentials (`gws auth status` should show the path; typically ~/.config/gws/ on Unix, %APPDATA%\\gws\\ on Windows). chmod 600 on Unix; on native Windows, restrict ACLs to my user with `icacls "$creds_path" /inheritance:r /grant:r "$env:USERNAME:F"` — file is already in my user profile so this needs no admin. -8. Verify with two low-impact reads, one per scope group: `gws drive files list --params '{{"pageSize": 1}}'` (Drive scope landed) and `gws chat spaces list --params '{{"pageSize": 1}}'` (Chat scope landed). If both return 200 with valid JSON, print `✅ Google Workspace ready — connected as . drive file(s), chat space(s) visible.` (exact prefix — the final summary grep for it). On any failure, print `❌ Google Workspace setup failed: , HTTP/status . .` and stop. Never echo tokens, file/message metadata, or scope strings to chat. +8. Verify with two low-impact reads, one per scope group: `gws drive files list --params '{{"pageSize": 1}}'` (Drive scope landed) and `gws chat spaces list --params '{{"pageSize": 1}}'` (Chat scope landed). Treat exit code 0 from each invocation as success — do NOT pipe gws output into `python3 -c 'f"..."'` (f-string expressions reject backslashes in Python <3.12, so escaping `\\"files\\"` inside a shell-quoted f-string raises SyntaxError) and do NOT call `json.load(sys.stdin)` on the raw stream (gws may emit log lines or a banner before the JSON body, which trips `JSONDecodeError`). If you really need to count rows for diagnostics, write the stdout to a temp file first and parse it with a plain `json.loads(open(path).read())` inside a `try/except`. If both calls exit 0, print `✅ Google Workspace ready — connected as . Drive + Chat scopes verified.` (exact prefix — the final summary grep for it). On any failure, print `❌ Google Workspace setup failed: , exit . .` and stop. Never echo tokens, file/message metadata, or scope strings to chat. 9. Remind me how to revoke later: `gws auth logout` clears local creds; the OAuth grant also appears at https://myaccount.google.com/permissions for Google-side revocation."""