docs/aws-sso.md: setup guide, modes per device type, log reference, troubleshooting
Also: unprompted-login back-off now matches the guide (30 min, 1 h, 2 h, then 4 h; the first retry was 1 h); README links the guide.
This commit is contained in:
+180
@@ -0,0 +1,180 @@
|
||||
# AWS SSO keeper (`extensions/aws-sso.ts`) — setup and operation
|
||||
|
||||
The keeper keeps pi's Bedrock access alive across AWS IAM Identity Center (IdC)
|
||||
session ends, so a long or overnight run does not stop at "Token is expired". It
|
||||
works like Claude Code's `awsAuthRefresh`: when the session ends it runs
|
||||
`aws sso login` for you. **It never approves anything itself.** Either your browser
|
||||
completes the login silently because it is still signed in, or you approve it, on
|
||||
the machine or on your phone.
|
||||
|
||||
Reference (commands, variables, mechanism): the `aws-sso.ts` section of the
|
||||
[README](../README.md). This page covers setup, daily use and troubleshooting.
|
||||
|
||||
## Why sessions end, and why a timer cannot fix it
|
||||
|
||||
- The **1-hour access token** is renewed by the AWS SDK by itself. You never see that.
|
||||
- The **session** behind it is your browser's AWS sign-in. Its maximum length is set
|
||||
by your IdC admin, and it is not predictable from the client: we measured 8 h on
|
||||
one day and at most 6 h on the next. When it ends, AWS refuses further renewals.
|
||||
- A login while the browser sign-in is still valid completes silently but **inherits
|
||||
the same end**, so renewing early gains nothing. Only a login after the end starts
|
||||
a new session.
|
||||
|
||||
So the keeper **detects** the end instead of guessing it. In the last 5 minutes of
|
||||
each 1-hour token it makes the renewal attempt itself, through the same SDK pi uses.
|
||||
If AWS refuses, it starts the login at once, while the old token still covers
|
||||
requests for a few minutes. If a request fails anyway (pi was closed, the laptop
|
||||
slept), it logs in and then sends `continue` so the interrupted run resumes.
|
||||
|
||||
The permanent fix is administrative. Your IdC admin can raise the session length
|
||||
(up to 90 days) in the IAM Identity Center console. With that, the keeper has almost
|
||||
nothing to do.
|
||||
|
||||
## Pick a mode for each device
|
||||
|
||||
| Situation | Mode | What a renewal looks like |
|
||||
|---|---|---|
|
||||
| Mac you sit at; pi-devbox runs on it | `local` (default) | A browser tab opens on the Mac and normally completes by itself ("Your credentials have been shared successfully…"). If it needs you (MFA), you get a push. |
|
||||
| Linux/Windows desktop with a browser | `local` | Same idea, but **not verified yet**: starting the desktop browser from an SSH session may need a host-specific `PI_AWS_SSO_LOCAL_CMD`. |
|
||||
| Headless server; you `ssh` in and run pi in tmux | `remote` | Push to your phone with a link and code. Tap it, check the code, press **Allow**. AWS always asks for that click in this flow. |
|
||||
| Always-on Mac used as a server (you are not at it) | `remote` | As above. `local` would open a tab on a screen nobody looks at. |
|
||||
|
||||
Set the mode with `/aws-sso local` or `/aws-sso remote` (stored per device), or
|
||||
with `PI_AWS_SSO_MODE` in `.env`. A mode set by command wins over `.env`.
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Prerequisites
|
||||
|
||||
- `AWS_PROFILE` in pi-devbox `.env` names your Bedrock profile. Its
|
||||
`source_profile` chain must end in a profile with `sso_session = …`, with a
|
||||
matching `[sso-session …]` block in `~/.aws/config`. Old-style profiles with
|
||||
`sso_start_url` directly in the profile cannot be renewed; convert them.
|
||||
- `~/.aws` is available in the container (pi-devbox mounts it).
|
||||
- **`local` only:** the Docker host has the AWS CLI, and the container can reach
|
||||
the host over SSH. Check from the container:
|
||||
|
||||
```sh
|
||||
ssh -F ~/.ssh-local/config host 'exec "${SHELL:-/bin/sh}" -lc "aws --version"'
|
||||
```
|
||||
|
||||
### 2. Push notifications (recommended; essential for `remote`)
|
||||
|
||||
1. Install the **ntfy** app (iOS/Android) on your phone.
|
||||
2. Make a topic name that nobody can guess. **Anyone who knows it can read your
|
||||
pushes.** Generate it in your own terminal, not in a pi session (transcripts
|
||||
are archived):
|
||||
|
||||
```sh
|
||||
echo "pi-aws-$(openssl rand -hex 12)"
|
||||
```
|
||||
|
||||
3. In the app, subscribe to that topic on `ntfy.sh` (or on your own ntfy server).
|
||||
|
||||
### 3. `.env` (pi-devbox)
|
||||
|
||||
```sh
|
||||
PI_AWS_SSO_NTFY=https://ntfy.sh/pi-aws-<your random part>
|
||||
PI_AWS_SSO_MODE=remote # only on headless/remote devices
|
||||
# PI_AWS_SSO_NTFY_TOKEN= # only for a protected topic on your own server
|
||||
# PI_AWS_SSO_REFRESH=1 # force the keeper on without /aws-sso on
|
||||
```
|
||||
|
||||
`.env` reaches the container through `env_file`. Recreate the container afterwards
|
||||
(`docker compose up -d`); a running pi does not see new variables.
|
||||
|
||||
### 4. In pi
|
||||
|
||||
```text
|
||||
/aws-sso on # once per device; stored in ~/.pi/agent/aws-sso/state.json
|
||||
/aws-sso remote # if this device should use the device-code flow
|
||||
/aws-sso push-test # your phone should buzz
|
||||
/aws-sso status
|
||||
/aws-sso now # optional: a first login, to see the flow once
|
||||
```
|
||||
|
||||
`/aws-sso status` shows, for example:
|
||||
|
||||
```text
|
||||
keeper: ON · local (default) · push: ntfy.sh (topic 3fb657b1) · auto-continue: on
|
||||
sso-session: MySession (profile chain Bedrock → Base)
|
||||
session: alive · started ≈ 22:19 · 1-h token until 23:19 (next check from 23:14)
|
||||
last check: 23:14 · refreshed
|
||||
last login: 2026-10-11 22:19:24 · ok (local)
|
||||
```
|
||||
|
||||
The topic is shown only as a fingerprint, never in clear.
|
||||
|
||||
## What happens when a session ends
|
||||
|
||||
1. **Detected:** in the 5 minutes before the hourly token runs out, AWS refuses the
|
||||
renewal. Log: `probe … result: refused`.
|
||||
2. **Login starts:**
|
||||
- `local`: a tab opens on the host. If it has not finished after 60 s, you get
|
||||
the push "AWS SSO login waiting".
|
||||
- `remote`: the link and code appear in pi, and you get the push "AWS SSO
|
||||
approval needed" (tap to open).
|
||||
3. **Done:** the log shows `login_end … ok: true`. If you were pushed about the
|
||||
login, a low-priority push "AWS SSO renewed" follows. A running task never
|
||||
noticed.
|
||||
4. **If nobody approves:** the old token runs out, the task's next request fails
|
||||
and the run pauses. The keeper tries again after 30 min, then 1 h, 2 h, and then
|
||||
every 4 h, so an unattended night produces only a few pushes. Each attempt brings
|
||||
a fresh code, and the keeper waits up to 10 minutes for each. When you approve,
|
||||
even hours later, the run resumes with `continue`.
|
||||
|
||||
Typing a prompt while the session is over, or `/aws-sso now`, starts a login at
|
||||
once without waiting for the back-off.
|
||||
|
||||
The status line shows `AWS ✓ since 22:19`, `AWS ⏳ approve ABCD-EFGH`,
|
||||
`AWS ⏳ waiting for you in the browser` or `AWS ✗ session ended`.
|
||||
|
||||
## Reading the log
|
||||
|
||||
`~/.pi/agent/aws-sso/events.jsonl`: one JSON line per event. Tokens appear only as
|
||||
`sha256[:8]` fingerprints.
|
||||
|
||||
| Event | Meaning |
|
||||
|---|---|
|
||||
| `anchor` | A new refresh token was seen. `inherited: true` = silent renewal inside the same session; `false` = new session |
|
||||
| `probe` | Renewal attempt inside the 5-min window: `refreshed` (fine), `refused` (session over), `unavailable` (pi's AWS SDK could not be loaded; says nothing about the session) |
|
||||
| `login_trigger`, `login_start` | A login started, with the reason and mode |
|
||||
| `device_code_shown` | `remote`: the code shown to you |
|
||||
| `login_end` | `ok: true` = a new refresh token arrived; otherwise `detail` has the CLI's last line |
|
||||
| `push`, `push_failed` | Push sent (HTTP status) or not (error) |
|
||||
| `request_failed_expired` | A request failed on an expired session (fallback path) |
|
||||
| `auto_continue` | The keeper sent `continue` to resume the run |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Check |
|
||||
|---|---|
|
||||
| Status `AWS ✗ AWS_PROFILE is not set` / `profile … not found` | `AWS_PROFILE` in `.env`, then the profile chain in `~/.aws/config` (step 1). |
|
||||
| `local` login ends with `rc: 255` | SSH to the host fails; run the check in step 1. |
|
||||
| `local` login hangs, no browser appears | Linux/Windows host: the SSH session cannot reach the desktop browser. Set `PI_AWS_SSO_LOCAL_CMD`, or use `remote`. |
|
||||
| No push arrives | `push`/`push_failed` lines in the log; the phone is subscribed to exactly that topic and server; the ntfy app is allowed to notify (iOS Focus / Android battery settings). `/aws-sso push-test`. |
|
||||
| "another pi process is already running the login" | Several pi windows share one login; approve the one shown in the window that started it. A stale lock from a crashed pi is cleared automatically. |
|
||||
| Nothing happens in a `task`/`fork` child or `pi -p` | By design: only interactive pi sessions act. |
|
||||
| A run stopped and did not resume | `auto_continue` in the log? It fires at most once per 10 min, and `/aws-sso continue off` disables it. Type `continue` yourself. |
|
||||
|
||||
## Security notes
|
||||
|
||||
- The keeper never handles passwords or MFA. It runs the official AWS CLI for
|
||||
logins, lets pi's own AWS SDK make its normal renewal attempt, and reads the token
|
||||
cache only for fingerprints and expiry times. Token values are never logged.
|
||||
- The ntfy topic is a shared secret: keep it long and random, or use a protected
|
||||
topic on your own server (`PI_AWS_SSO_NTFY_TOKEN`).
|
||||
- A pushed device link does not give anyone your token. Whoever approves it signs
|
||||
the waiting CLI in with **their own** account. Still, approve only codes you
|
||||
expect, and check that the code in the browser matches the push.
|
||||
|
||||
## Verified so far (2026-10-11, mbp-m1-2020)
|
||||
|
||||
| Item | Status |
|
||||
|---|---|
|
||||
| `local` on a macOS host, browser still signed in: silent, ~3–7 s | verified |
|
||||
| `local` after the session ended: browser re-signed in through the IdP silently, run auto-continued | verified once (request-failed path) |
|
||||
| Detection by refresh attempt before any request fails | unit-tested; first live run pending |
|
||||
| ntfy push: delivery, title, priority, tap link, UTF-8 body | verified against ntfy.sh |
|
||||
| `remote` end to end (code → push → approve on phone) | not yet |
|
||||
| `local` on Linux/Windows hosts | not yet |
|
||||
Reference in New Issue
Block a user