Files

194 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (measured 8 h here). In its last hour AWS still issues new tokens
but caps them at the session's end; after the end it refuses 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.
- AWS sometimes refuses a renewal while the session still has time left (cause
unknown): seen once by the keeper's check, 5 min before the end, and once as a failed
request about 2 h before it. A login then worked at once.
So the keeper **reads** 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:
- **new full token:** the session goes on;
- **new token cut short:** its expiry is the session's end, and the keeper logs in
30 s after it (the cut-short token covers requests until then);
- **refused:** it logs in at once, while the old token still covers requests.
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 since ≈ 00:47 · ends 08:47 (AWS caps tokens there) → login just after
token: valid until 08:47 · next check from 08:42
last check: 07:43 · capped: session ends 08:47
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 session's last hour the hourly check gets a token cut short at
the end (`probe … result: capped`, then `session_end_seen`), and the status line
shows `AWS ✓ until 08:47`. The login starts 30 s after that time. If AWS refuses
the renewal instead (`probe … result: refused`), the login starts at once.
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 (between successful logins it waits only 2 min), 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 (a login). `token_min` = its token's lifetime: 60 = a new session; less (`inherited: true`) = it joined the ending session |
| `probe` | Renewal attempt inside the 5-min window: `refreshed` (new full token), `capped` (token cut at the session end, `ends`), `refused` (nothing issued: log in now), `unavailable` (pi's AWS SDK could not be loaded; says nothing about the session) |
| `session_end_seen` | The session's end read from a capped token |
| `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 (mbp-m1-2020, macOS host)
| 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) |
| Session end detected before any request failed; unattended silent login (00:13, 00:47) | verified overnight 2026-10-12, but the 00:47 login came 29 min after the end (fixed: now 30 s after) |
| End read from the token cap 39 min ahead; login 45 s after it, silent, new full-hour session, no failed request | verified 2026-10-12 08:08–08:49 |
| 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 |