194 lines
10 KiB
Markdown
194 lines
10 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 (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 |
|