From 16b2532e92347168b78bed9dd9565f5685c3106f Mon Sep 17 00:00:00 2001 From: Siddartha Aralakuppe Yogesha Date: Sat, 24 Oct 2026 15:55:00 +0530 Subject: [PATCH 01/13] [failproofai-config-account-login] Log in to FailproofAI Cloud from `failproofai config`, and give every agent the failproofai skill (#920) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [failproofai-config-account-login] Log in to FailproofAI Cloud from `failproofai config` The CONNECT step opens with a menu: log in through the browser (default), paste an API key, or use open source. Logging in opens a link (or prints it over SSH / with no display) where a person signs up or in, picks or creates an org and authorizes the machine; the CLI polls with a secret only it holds and shows who approved it, for which org. The key is claimed at APPLY, after the daemon installs, so an abandoned run mints nothing. A connected machine is offered "keep" first, or "log in again" first when its saved key no longer works. - src/hooks/cloud-login.ts: start / poll / claim against /api/auth/cli/*, injectable fetch, no redirects followed, only http(s) links ever handed to the opener; older clouds read as unsupported - tui promptBrowserLogin: link printed above the live region (never cut), esc back, ctrl-c cancel, expiry/deny offer a new link, keeps polling through a dropped connection - configure-wizard: Connect menu, claim at apply, re-login note, sudo re-asked at apply when the cache expired during the login - open-browser: canOpenBrowser (false over SSH / no display), openUrl exported - e2e harness __tests__/e2e/layout/cli-login.sh + pty driver - docs: quickstart and CLI reference lead with the login Co-Authored-By: Claude Opus 5.5 * Changelog: PR number for CLI login (#920) Co-Authored-By: Claude Opus 5.5 * [config-account-login] Match the simpler authorize page; re-ask sudo before a unit refresh - The terminal now says "Not authorized in the browser." when the person picks [ don't authorize ] on the page (was "The request was cancelled in the browser."), matching the page's vocabulary; the e2e harness waits for it. - The quickstart no longer asks people to check the key's name and permissions: the page stopped showing them, the server sets both. - The e2e harness approves without a `jev` flag, exactly as the page does now, and still asserts the key gets jev:evaluate because the approver holds it. - CodeRabbit: the sudo re-ask before apply also covers refreshing a stale service definition; failing it there costs the scheduled audit (caution), never the setup. The reference doc no longer says setup "never" prompts for a password: a terminal run does, and asks again if it expired. Co-Authored-By: Claude Opus 5.5 * [config-account-login] Start the login by itself; harden what the server sends Connect to cloud on a machine with no connection (or a refused saved key) now starts the browser login straight away — link generated, browser opened or link printed, countdown running. Tab or Esc there opens the other options; Ctrl-C cancels. A machine connected with a working key still gets the menu, keep first, so re-running config never opens a browser. An older cloud drops straight into the options without the login entry. Leaving the waiting screen erases the link it printed (rows counted at the current width), so no dead link sits above the menu. Deep-scan fixes: - Everything the server sends is printed as text only: org name and slug, approver, key name, errors (controls, bidi overrides and zero-width characters stripped — an org named with escape codes cleared the screen and retitled the terminal mid-setup), and the link as the URL parser serialises it. Same for the introspect path's "Connected as". - The server's poll interval and expiry are clamped (an interval past 2^31 ms made setTimeout fire every millisecond); slow_down never speeds up. - A saved cloud address is validated like a typed one. - A key that was created but could not be put to use is named, so it can be revoked; a "log in again" that created no key reports the previous connection, not open source. Co-Authored-By: Claude Opus 5.5 * [config-account-login] A failed claim on a fresh machine is not "kept" savedConnection() returns null, not undefined, so `saved !== undefined` was always true: a fresh machine whose login claim failed printed "This machine keeps its previous connection." and reported mode "cloud" with no credential on disk. Truthiness, like every other check of `saved` here. The fresh-machine claim-failure test now asserts mode "oss" and the absence of that line (CodeRabbit, PR #920). Co-Authored-By: Claude Opus 5.5 * [config-account-login] Release as 1.0.12-beta.1 1.0.12-beta.0 is already on npm (published from this branch on `next`, built from a42e75f5), so publish preflight would refuse it from main. Move package.json, the Cargo workspace (+ Cargo.lock) and the Hermes plugin manifest to 1.0.12-beta.1, and give it its own changelog section with the one change the published beta.0 lacks (b7aaddd3). Co-Authored-By: Claude Opus 5.5 * [config-account-login] A refused saved key is not kept as if it worked When the saved key was refused and the login that followed created no key, the run ended with "This machine keeps its previous connection." The connection is still kept (the CONNECT step never removes one, decision D4, same as picking "keep" with a refused key), but the line now says its key no longer works and to run config again. The verdict from the CONNECT step is carried to the end as `savedRefused`. Regression test fails without the fix (CodeRabbit, PR #920). Co-Authored-By: Claude Opus 5.5 * [config-account-login] Close the remaining CodeRabbit / Hermes findings - checkCloudKey validates the address before sending a key to it: a saved http:// address to another host no longer receives the saved key when config checks it (CodeRabbit, outside-diff, b7aaddd3). Five older tests used the placeholder address "u", which the guard now refuses; they use the valid https address their neighbours already use. - configure_applied is sent after the cloud step with the final `connected` and mode, so a login whose claim failed is not counted as connected (CodeRabbit + Hermes F4). - promptBrowserLogin: a retry erases the expired/refused link (and its heading) before printing the new one — `forgetAbove()`, shared with leaving on Tab/Esc (CodeRabbit + Hermes F3). - startLogin: a 200 that is not JSON (the web app serving a page in the route's place) is `unsupported`, so the key prompt is offered (CodeRabbit). - connectToCloud takes `org` from the caller when introspect gives none; the wizard passes the org from the login's claim, so the saved org is not left empty or stale (CodeRabbit). Each new test fails without its fix. Co-Authored-By: Claude Opus 5.5 * [config-account-login] Release as 1.0.12 (stable) 1.0.12-beta.1 was published to npm on `next` from another branch (feat/cloud-jev-on-main) before this PR took that number, so publishing it from main would be refused by preflight. Cut the stable release instead: 1.0.12 across package.json, Cargo.toml/Cargo.lock and hermes-plugin/plugin.yaml, and the top changelog section becomes `## 1.0.12`, which the stable release-notes check and the Discord announcement read along with every `1.0.12-beta.*` section below it. Co-Authored-By: Claude Opus 5.5 * [config-account-login] Give every agent the failproofai skill `config` now writes one small `failproofai/SKILL.md` into each agent it finds (and any ticked), in that agent's own user-scope skills folder. The file is a pointer: it sends the agent to the umbrella skill and its five siblings in the public FailproofAI/skills repo, so the content is read live and never goes stale on disk. Every folder was verified against the real CLI in Docker (probe skills, the CLI's own listing or a mock-model capture), then re-checked end to end with this code for Claude Code, Codex, Copilot, OpenCode, OpenClaw, Devin and Goose: - one folder per agent, following CLAUDE_CONFIG_DIR, CODEX_HOME, COPILOT_HOME, XDG_CONFIG_HOME, PI_CODING_AGENT_DIR, FACTORY_HOME_OVERRIDE and GOOSE_PATH_ROOT; every Hermes and OpenClaw profile gets its own copy; - never the shared ~/.agents/skills (Claude ignores it, Codex lists dupes, Copilot drops it under COPILOT_HOME, OpenClaw lets it outrank its own); - name = folder name, description under 200 chars and led by its subject (Copilot drops >1024, droid shows 200, Hermes 57). A `.failproofai-managed` marker marks folders failproofai wrote: one without it (e.g. `npx skills add FailproofAI/skills`) is never overwritten, and `failproofai uninstall` removes only marked folders. A folder that can't be written is logged and skipped; setup never fails on it. Co-Authored-By: Claude Opus 5.5 * [config-account-login] Add the failproofai skill silently Drop the "Added the failproofai skill to N agents." line from config's summary; the summary stays about enforcement and the connection. Co-Authored-By: Claude Opus 5.5 * [config-account-login] Don't give Codex the failproofai skill twice Codex reads ~/.agents/skills as well as its own ~/.codex/skills and lists a name found in both twice (0.164.0). `npx skills add FailproofAI/skills -g` puts the full skill in ~/.agents/skills for Codex, so a machine with that and `failproofai config` showed Codex two `failproofai` skills. When ~/.agents/skills/failproofai exists, config now leaves Codex to it: it writes no Codex copy, and removes one it wrote on an earlier run. Checked in Docker against real Codex: npx then config lists it once; config then npx lists it twice until config runs again, which takes ours back. Co-Authored-By: Claude Opus 5.5 * [config-account-login] Never stop setup on removing an old Codex skill copy Taking back our Codex copy (when ~/.agents/skills already serves Codex) called rmSync outside any try, so a folder that could not be removed would throw out of installAgentSkill and fail `config`, breaking its never-fails-setup contract. Route it, and uninstall's removal, through one removeSkillDir that logs and carries on. Co-Authored-By: Claude Opus 5.5 * [config-account-login] Report the kept connection when a new login's key can't be used Logging in again on a connected machine, with the key claimed but refused on both streams, made connectToCloud write nothing, so the previous connection stayed on disk and working. `keptOld` tested `!token`, though, so the summary and configure_applied reported the machine as open source. Test `!connected` instead. A refused saved key still counts as kept (mode cloud, decision D4), with its own message. (CodeRabbit) Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- CHANGELOG.md | 26 + Cargo.lock | 6 +- Cargo.toml | 2 +- __tests__/e2e/layout/README.md | 6 + __tests__/e2e/layout/cli-login-driver.py | 102 ++++ __tests__/e2e/layout/cli-login.sh | 284 ++++++++++ __tests__/hooks/agent-skill.test.ts | 193 +++++++ __tests__/hooks/browser-login-prompt.test.ts | 233 ++++++++ __tests__/hooks/cloud-login.test.ts | 254 +++++++++ __tests__/hooks/cloud-reconnect.test.ts | 14 + __tests__/hooks/configure-wizard.test.ts | 550 ++++++++++++++++++- __tests__/hooks/uninstall-cli.test.ts | 37 ++ docs/reference/failproof-cli.mdx | 10 +- docs/start/quickstart.mdx | 29 +- hermes-plugin/plugin.yaml | 2 +- package.json | 2 +- src/audit/open-browser.ts | 20 +- src/hooks/agent-skill.ts | 193 +++++++ src/hooks/cloud-connection.ts | 24 +- src/hooks/cloud-introspect.ts | 7 +- src/hooks/cloud-login.ts | 248 +++++++++ src/hooks/configure-wizard.ts | 388 +++++++++++-- src/hooks/tui.ts | 280 ++++++++++ src/hooks/uninstall-cli.ts | 13 +- 24 files changed, 2826 insertions(+), 97 deletions(-) create mode 100755 __tests__/e2e/layout/cli-login-driver.py create mode 100755 __tests__/e2e/layout/cli-login.sh create mode 100644 __tests__/hooks/agent-skill.test.ts create mode 100644 __tests__/hooks/browser-login-prompt.test.ts create mode 100644 __tests__/hooks/cloud-login.test.ts create mode 100644 src/hooks/agent-skill.ts create mode 100644 src/hooks/cloud-login.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d4f9e68b9..24cc6c4af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,33 @@ # Changelog +## 1.0.12 — 2026-10-23 + +The stable release of logging in to FailproofAI Cloud from `config` in your browser. Everything below in the `1.0.12-beta.*` sections ships here too. + +### Features + +- **Every agent on the machine knows FailproofAI.** `config` adds a small `failproofai` skill to each agent it finds (and any you tick), in that agent's own skills folder: Claude Code, Codex, Copilot, Cursor, OpenCode, Pi, every Hermes and OpenClaw profile, droid, Devin, Antigravity and Goose. It points the agent at the FailproofAI skills on GitHub, which it reads when a FailproofAI task comes up, so what it reads is always the current version with nothing to update. Each folder was checked against the agent itself. A `failproofai` skill you installed yourself (for example with `npx skills add FailproofAI/skills`) is left as it is, and Codex, which would list one in `~/.agents/skills` beside its own copy, is left to that one. `failproofai uninstall` removes only the ones `config` wrote (#920) + +### Fixes + +- `config` on a machine with no saved connection, whose browser login was approved but whose key could not be claimed, no longer says "This machine keeps its previous connection." and reports cloud mode with no key on disk; it finishes set up as open source (#920) +- `config`, logging in again on a connected machine where the new key is created but can't be used, says the machine keeps its previous connection (which it does: nothing was written) instead of finishing as if it were open source (#920) +- `config` on a machine whose saved key was refused, when logging in again creates no key, says the kept connection's key no longer works and to run `config` again, instead of reporting the old connection as if it were fine (#920) +- `config` no longer sends the saved key to a saved cloud address it would refuse if typed (plain `http://` to another host) when checking that key (#920) +- `config`'s browser login takes an expired or refused link off the screen when it prints a new one, so only the live link can be opened; and a cloud that answers the login route with a web page is treated as one without browser login (paste a key) instead of an error (#920) +- `config` saves the org a browser login was approved into even when the follow-up check of the new key can't reach the server, instead of leaving the saved org empty or stale; and its completion event reports whether the machine actually connected, not whether it set out to (#920) + ## 1.0.12-beta.0 — 2026-10-22 +### Features + +- **Log in to FailproofAI Cloud from `config`.** On a machine that is not connected (or whose saved key is refused), the connect step now starts a browser login straight away; Tab opens the other options: paste an API key, or use open source. Logging in opens a link — or prints it over SSH or with no display, to open on any device — where you sign up or sign in, pick or create an org, and authorize the machine; the terminal notices on its own and shows who approved it and for which org. The machine's key is created only when setup finishes, after the daemon is installed, so a run you abandon leaves no key behind. A machine that is already connected with a working key gets a menu with "keep" first, so re-running setup never opens a browser by itself. Needs a FailproofAI Cloud with browser login; an older one says so and pasting a key still works (#920) + +### Fixes + +- `config`'s cloud login prints what the server sends — org, approver, key name, errors, the link — without terminal control or bidi characters, keeps the server's poll interval and expiry to a sane range, checks a saved cloud address like a typed one, and names the key to revoke if one was created but could not be used (#920) +- `config` asks for your sudo password again before installing the daemon, or refreshing its service definition, when the one given at the start expired while you were answering (5 minutes on macOS by default), instead of failing after everything else was done (#920) + ### Dependencies - Combine the open Dependabot PRs (#921, #922, #923) into one: `certifi` 2026.7.22 → 2026.10.19 (GHSA-6q48-6c7w-9gw2) in `fp-cloud-cli` and the Python SDK's test lockfile, and `vite` 8.3.0 → 8.3.4 in the TypeScript SDK's dev lockfile. The Supply Chain gate scans every lockfile, so none of the three could pass on its own (#926) diff --git a/Cargo.lock b/Cargo.lock index a8cbb1178..c16256b72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -298,7 +298,7 @@ dependencies = [ [[package]] name = "failproofaid" -version = "1.0.12-beta.0" +version = "1.0.12" dependencies = [ "fpai-collect", "fpai-ipc", @@ -361,7 +361,7 @@ dependencies = [ [[package]] name = "fpai-collect" -version = "1.0.12-beta.0" +version = "1.0.12" dependencies = [ "axum", "getrandom 0.4.3", @@ -382,7 +382,7 @@ dependencies = [ [[package]] name = "fpai-ipc" -version = "1.0.12-beta.0" +version = "1.0.12" dependencies = [ "libc", "proptest", diff --git a/Cargo.toml b/Cargo.toml index 62a65a004..beff85f1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "3" members = ["crates/*"] [workspace.package] -version = "1.0.12-beta.0" +version = "1.0.12" edition = "2024" license-file = "LICENSE" repository = "https://github.com/FailproofAI/failproofai" diff --git a/__tests__/e2e/layout/README.md b/__tests__/e2e/layout/README.md index 8ec809f6b..ddb2c2d01 100644 --- a/__tests__/e2e/layout/README.md +++ b/__tests__/e2e/layout/README.md @@ -9,6 +9,7 @@ not depend on — so they are invoked deliberately. | `layout-reset.sh` | nothing | fresh/stale/future layout detection, reset semantics, OSS silence, hook warn-don't-delete-don't-deny | | `cloud-collector.sh` | an AgentEye stack | real connect against a live server, daemon + collector writing layout-2 paths, decisions reaching ClickHouse, disconnect back to OSS | | `systemd-service.sh` | Docker | REAL `systemctl enable --now` as root in a privileged container: service lifecycle, fail-closed, self-heal, reinstall | +| `cli-login.sh` | an AgentEye stack (dashboard + mailpit + its Postgres), Docker, `bun run build` + the release daemon | `config`'s browser login through a real pty in a systemd container: approve, re-login, deny + retry, abandon after approval (no key minted), esc, expiry, revoked saved key, older server, and the person-side refusals (Origin, form posts, no session, start rate limit) | `systemd-service.sh` exists because the service half cannot be tested on a developer machine without a password prompt, and a container gives real systemd @@ -31,6 +32,11 @@ Two things it caught that a mock could not: bash __tests__/e2e/layout/layout-reset.sh bash __tests__/e2e/layout/systemd-service.sh +# cli-login.sh drives the real wizard against a running AgentEye; point it at +# that stack's dashboard, mailpit API and Postgres container. +DASH=http://localhost:3000 MAILPIT=http://localhost:8026 PG_CONTAINER=agenteye-postgres-1 \ + bash __tests__/e2e/layout/cli-login.sh + # cloud-collector.sh expects the stack on the remapped ports (18080/18123) so it # can run alongside an existing project rather than displacing it: docker compose -p aefpai -f docker-compose.yml -f ports.yml up -d diff --git a/__tests__/e2e/layout/cli-login-driver.py b/__tests__/e2e/layout/cli-login-driver.py new file mode 100755 index 000000000..45a465918 --- /dev/null +++ b/__tests__/e2e/layout/cli-login-driver.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Drive `failproofai config` through a real pseudo-terminal, screen by screen. + +Used by cli-login.sh, inside the systemd container, as the unprivileged user. +The wizard only offers browser login to a real terminal, so a pty is the only +honest way to test it end to end. + +Usage: cli-login-driver.py