40643454f0
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.
181 lines
9.4 KiB
Markdown
181 lines
9.4 KiB
Markdown
# 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 |
|