Summary
scopehold connect aborts and saves nothing when the agent is approved with the session token lifetime, because the token response (correctly) contains no refresh token. The agent is created server-side, but the local profile is never written, so every later command fails with "profile not found."
Repro
scopehold connect --profile demo
- Approve in the browser and choose the session lifetime.
- Observe:
Waiting for approval...
ScopeHold OAuth response did not include refresh_token.
ScopeHold profile "demo" was not found. Run scopehold connect first.
The agent appears in Connected Agents but shows as already expired.
Root cause (expected server behavior, not an API bug)
The token endpoint only mints a refresh token when the refresh window outlives the access token. For session the refresh window equals the 1h access TTL, so no refresh token is issued:
- Lifetimes + TTLs — API
apps/api/src/oauth/device-flow.ts: session → 3600s, 8h, 7d (default), persistent → null. Access TTL ceiling OAUTH_ACCESS_TOKEN_TTL_SECONDS = 3600.
- No-refresh rule — API
apps/api/src/oauth/service.ts (issueOAuthTokenPair): issueRefresh = refreshExpiresAt === null || accessExpiresAt < refreshExpiresAt. For session they are equal, so issueRefresh = false and the response carries an access token only.
So session is by design a ~1-hour, access-only credential. The CLI simply has no path to persist one and treats a missing refresh token as fatal.
Proposed fix (CLI)
When the token response has no refresh token, save an access-only profile:
- Persist the access token + its expiry; mark the profile non-refreshable.
- Use it normally for
status / inventory / resolve / run until the access token expires (~1h).
- On expiry, print a clear "session expired, re-run
scopehold connect" message instead of attempting a refresh. This matches the documented intent (API apps/api/src/agent-guidance/content.ts: "re-run scopehold connect ... the approval screen pre-links your existing agent").
Acceptance criteria
connect with session writes a usable profile (no hard error).
- Commands work until the 1h access token expires.
- After expiry, the CLI prints a clear re-connect prompt, never a confusing "profile not found."
connect with 8h / 7d / persistent is unchanged (refresh token persisted as today).
Related (not required for this fix)
- Consent screen UX: default is
7d (safe), but session is easy to pick and silently means "1h, re-approve each session." Consider relabeling (e.g. "Session — 1 hour, no auto-refresh") or warning when it is chosen for a CLI/MCP connect.
- Remote MCP parity: the
/mcp auth-code path (API apps/api/src/routes/oauth.ts) calls the same issueOAuthTokenPair, so session yields no refresh token there too. MCP clients degrade gracefully (they re-authorize on expiry per the agent guidance) rather than hard-failing like the CLI, but the same relabel/warning would help.
Summary
scopehold connectaborts and saves nothing when the agent is approved with thesessiontoken lifetime, because the token response (correctly) contains no refresh token. The agent is created server-side, but the local profile is never written, so every later command fails with "profile not found."Repro
scopehold connect --profile demoThe agent appears in Connected Agents but shows as already expired.
Root cause (expected server behavior, not an API bug)
The token endpoint only mints a refresh token when the refresh window outlives the access token. For
sessionthe refresh window equals the 1h access TTL, so no refresh token is issued:apps/api/src/oauth/device-flow.ts:session → 3600s,8h,7d(default),persistent → null. Access TTL ceilingOAUTH_ACCESS_TOKEN_TTL_SECONDS = 3600.apps/api/src/oauth/service.ts(issueOAuthTokenPair):issueRefresh = refreshExpiresAt === null || accessExpiresAt < refreshExpiresAt. Forsessionthey are equal, soissueRefresh = falseand the response carries an access token only.So
sessionis by design a ~1-hour, access-only credential. The CLI simply has no path to persist one and treats a missing refresh token as fatal.Proposed fix (CLI)
When the token response has no refresh token, save an access-only profile:
status/inventory/resolve/rununtil the access token expires (~1h).scopehold connect" message instead of attempting a refresh. This matches the documented intent (APIapps/api/src/agent-guidance/content.ts: "re-runscopehold connect... the approval screen pre-links your existing agent").Acceptance criteria
connectwithsessionwrites a usable profile (no hard error).connectwith8h/7d/persistentis unchanged (refresh token persisted as today).Related (not required for this fix)
7d(safe), butsessionis easy to pick and silently means "1h, re-approve each session." Consider relabeling (e.g. "Session — 1 hour, no auto-refresh") or warning when it is chosen for a CLI/MCP connect./mcpauth-code path (APIapps/api/src/routes/oauth.ts) calls the sameissueOAuthTokenPair, sosessionyields no refresh token there too. MCP clients degrade gracefully (they re-authorize on expiry per the agent guidance) rather than hard-failing like the CLI, but the same relabel/warning would help.