Skip to content
Open
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
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
6 changes: 6 additions & 0 deletions __tests__/e2e/layout/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
102 changes: 102 additions & 0 deletions __tests__/e2e/layout/cli-login-driver.py
Original file line number Diff line number Diff line change
@@ -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 <script> <transcript-file> <url-file> -- <command...>

<script> is a comma-separated list of steps, each `wait:<text>` (block until
the screen shows <text>), `key:<name>` (enter, down, up, esc, ctrl-c, tab),
`url` (wait for the authorize link and write it to <url-file>), or
`sleep:<seconds>`. Exit status is the command's own, or 124 on a timeout.
"""
import os
import pty
import re
import select
import sys
import time

KEYS = {
"enter": b"\r",
"down": b"\x1b[B",
"up": b"\x1b[A",
"esc": b"\x1b",
"ctrl-c": b"\x03",
"tab": b"\t",
}
ANSI = re.compile(rb"\x1b\[[0-9;?]*[A-Za-z]|\x1b[78]")
LINK = re.compile(r"https?://\S+/cli/authorize\?r=[0-9a-f]{64}")
STEP_TIMEOUT = float(os.environ.get("DRIVER_STEP_TIMEOUT", "60"))


def main() -> int:
script, transcript_path, url_path = sys.argv[1], sys.argv[2], sys.argv[3]
cmd = sys.argv[sys.argv.index("--") + 1 :]
pid, fd = pty.fork()
if pid == 0:
os.environ["COLUMNS"] = "100"
os.execvp(cmd[0], cmd)

screen = b""
seen_from = 0 # `wait` only matches text that arrived after the last match

def pump(timeout: float) -> bool:
nonlocal screen
r, _, _ = select.select([fd], [], [], timeout)
if not r:
return True
try:
chunk = os.read(fd, 65536)
except OSError:
return False
if not chunk:
return False
screen += chunk
with open(transcript_path, "ab") as f:
f.write(chunk)
return True

def plain() -> str:
return ANSI.sub(b"", screen).decode("utf-8", "replace")

for step in [s for s in script.split(",") if s]:
kind, _, arg = step.partition(":")
deadline = time.time() + STEP_TIMEOUT
if kind == "wait":
while arg not in plain()[seen_from:]:
if time.time() > deadline or not pump(0.2):
print(f"driver: timed out waiting for {arg!r}", file=sys.stderr)
return 124
seen_from = plain().index(arg, seen_from) + len(arg)
elif kind == "url":
while not LINK.search(plain()[seen_from:]):
if time.time() > deadline or not pump(0.2):
print("driver: no authorize link appeared", file=sys.stderr)
return 124
m = LINK.search(plain()[seen_from:])
seen_from += m.end()
with open(url_path, "w") as f:
f.write(m.group(0))
elif kind == "key":
time.sleep(0.3) # let the screen settle so the key lands on it
os.write(fd, KEYS[arg])
elif kind == "sleep":
end = time.time() + float(arg)
while time.time() < end:
pump(0.2)

# Drain until the command exits.
deadline = time.time() + STEP_TIMEOUT * 2
while pump(0.2):
if time.time() > deadline:
print("driver: command did not exit", file=sys.stderr)
return 124
_, status = os.waitpid(pid, 0)
return os.waitstatus_to_exitcode(status)


if __name__ == "__main__":
sys.exit(main())
Loading
Loading