Skip to content

fix(agent-lifecycle): stop showing credentials_injection: failed for subscription agents (#612) - #663

Merged
vybe merged 1 commit into
devfrom
fix/612-credentials-injection-subscription
May 5, 2026
Merged

fix(agent-lifecycle): stop showing credentials_injection: failed for subscription agents (#612)#663
vybe merged 1 commit into
devfrom
fix/612-credentials-injection-subscription

Conversation

@dolho

@dolho dolho commented May 5, 2026

Copy link
Copy Markdown
Contributor

Closes #612

Summary

Subscription-mode agents authenticate via CLAUDE_CODE_OAUTH_TOKEN env var (SUB-002) and have no .credentials.enc file. The agent-start path ran inject_assigned_credentials unconditionally, the encryption service raised ValueError("No .credentials.enc file found in agent workspace"), and the caller's "not found" in str(e).lower() substring check missed it (the actual message contains "file found", not "not found").

The misleading {"credentials_injection": "failed"} leaks to the agent-start response and prompts operators to re-assign subscriptions or recreate containers — disrupting running state for no reason. We hit this exact false alarm earlier today restarting agent-trinity-system after a subscription assignment.

Fix

Two-part:

  1. CredentialsFileNotFoundError (ValueError subclass) in services/credential_encryption.py. import_to_agent raises this specific type when .credentials.enc is absent, so callers can catch it without fragile substring matching. Subclassing ValueError preserves the admin import endpoint's existing 400 behavior — that path should surface a real error when the operator explicitly asks for import.

  2. Subscription-mode short-circuit in inject_assigned_credentials. db.get_agent_subscription_id() check fires before the import path runs; returns {"status": "skipped", "reason": "subscription_mode", "detail": "agent authenticates via CLAUDE_CODE_OAUTH_TOKEN; file-based credential injection is not used"} with explicit mention of the env-var path so operators see this is by design.

The substring-match path is removed entirely — the explicit exception subclass replaces it.

Why two changes

Either alone would close the cosmetic bug, but together they're load-bearing:

  • Subscription-only fix: solves the reported case but leaves the substring-match landmine for any future error message change.
  • Exception-subclass-only fix: subscription agents would still call import_to_agent (a no-op for them) and waste retry budget on the doomed path.

Together they short-circuit the common case AND make the rare case (subscription agent that also has a .credentials.enc for non-Anthropic creds) report cleanly if it ever fails.

Tests

tests/unit/test_inject_assigned_credentials.py — 8 cases:

  • subscription mode short-circuits without consulting the service
  • file-missing returns skipped (not failed)
  • CredentialsFileNotFoundError remains a ValueError (admin path compat)
  • happy path returns success with file list
  • empty decrypted dict returns skipped
  • other ValueError shapes retry then surface as failed
  • transient ValueError recovers on retry
  • missing CREDENTIAL_ENCRYPTION_KEY returns skipped
$ python -m pytest tests/unit/test_start_agent_skip_inject.py tests/unit/test_inject_assigned_credentials.py tests/unit/test_credential_inject_allowlist.py -q
32 passed in 121.02s

Test plan

🤖 Generated with Claude Code

…subscription agents (#612)

Subscription-mode agents authenticate via CLAUDE_CODE_OAUTH_TOKEN env var
(SUB-002) and have no .credentials.enc file. The agent-start path ran
inject_assigned_credentials unconditionally, the encryption service raised
ValueError("No .credentials.enc file found in agent workspace"), and the
caller's "not found" substring check missed it (the actual message contains
"file found", not "not found"). The misleading status leaks to the start
endpoint response and prompts operators to re-assign subscriptions /
recreate containers — disrupting running state for no reason.

Two-part fix:

  1. CredentialsFileNotFoundError (ValueError subclass) in
     services/credential_encryption.py. import_to_agent now raises this
     specific type when .credentials.enc is absent, so callers can catch
     it without fragile substring matching. Subclassing ValueError
     preserves the admin import endpoint's existing 400 behavior — that
     path *should* surface a real error when the operator explicitly
     asked for an import.

  2. Subscription-mode short-circuit in
     inject_assigned_credentials. db.get_agent_subscription_id() check
     fires before the import path runs; returns
     {"status": "skipped", "reason": "subscription_mode", "detail": "..."}
     with explicit mention of CLAUDE_CODE_OAUTH_TOKEN so operators see
     this is by design.

The substring-match path is removed entirely — the explicit exception
subclass replaces it.

Tests:
  tests/unit/test_inject_assigned_credentials.py — 8 cases:
    - subscription mode short-circuits without consulting the service
    - file-missing returns skipped (not failed)
    - CredentialsFileNotFoundError remains a ValueError (admin path
      compat)
    - happy path still returns success with file list
    - empty decrypted dict returns skipped
    - other ValueError shapes retry then surface as failed
    - transient ValueError recovers on retry
    - missing CREDENTIAL_ENCRYPTION_KEY returns skipped

Combined with the existing #421 skip-inject tests: 32 tests pass, no
regression.

Issue: #612
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean two-part fix — exception subclass removes the fragile substring match and the subscription short-circuit fires before the service is even consulted. 8 unit tests cover all the edge cases. LGTM.

@vybe
vybe merged commit e07a255 into dev May 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants