feat(credentials): link agent roles to credential profiles #352

Merged
rwachters merged 8 commits from feat/credential-profile-agent-roles into master 2026-10-27 04:19:31 +00:00
rwachters commented 2026-10-27 04:16:01 +00:00 (Migrated from github.com)

Summary

Adds a per-agent-role authorization dimension to credential profiles on top of the existing per-worker one. A profile's link set is stored in a new join table, travels inside the app-signed profile body, is relayed to the worker, and is enforced when a terminal session attaches the profile, so only a linked role may use it. The app gains a role multi-select in the profile editor and shows each profile's linked roles.

Changes

  • Added agentRoleIds to the credential profile DTO and to both write requests, as part of the signed body.
  • Added the V38 migration and the (profile_id, agent_role_id) join table with cascades on both keys, inserting no rows.
  • Validated supplied role ids against the caller's own roles and wrote the link set atomically with the profile and its signature.
  • Scoped the credential_profiles prompt section to the intersection of worker and role link.
  • Split the process-scoped tool environment from the per-call execution context so the acting role reaches the terminal tool only.
  • Enforced the link in attach, refusing a role-less or unlinked caller before any decryption, and filtered the unknown-profile hint accordingly.
  • Signed the session's role into the built-in tool authorization from the app.
  • Added the role picker, the linked-role labels and the unusable-until-linked marker to the credential settings surface.
  • Grouped the credential-profile role picker by project with section headers, matching the spawnable-agent-role picker in the agent-roles tab.
  • Extracted the chip picker and the role label into one shared implementation used by both dialogs, so the two surfaces cannot drift apart in grouping or wording.

Implementation notes

  • The link set is unordered by design: the composite primary key makes duplicates impossible, both sides compare it as a set, and a save is a full replacement.
  • The worker rejects an attach with no role id before it even resolves the profile name, so a role-less caller cannot learn which profiles a worker holds; an unlinked role is rejected before the enabled flag is consulted.
  • Link rows are removed by the foreign-key cascade rather than by an explicit delete, which is correct on every connection the server opens because it enforces foreign keys.
  • In the grouped picker the project is carried by the section header, so chips are labelled name or name — displayName; profile-card labels keep their — project suffix because a card row has no header to carry that context.
  • The credential picker and the profile-card labels are both derived from the same role and project streams in one place, so a card cannot contradict the picker.

Deployment

  • Server, worker and app must be upgraded together; a partially upgraded deployment is unsafe in both directions.
  • A new worker with an old app denies every credential attach, because the old app signs no role.
  • An old worker with a new app silently loses the role enforcement entirely, because it ignores the field.
  • Existing credential profiles become unusable until at least one agent role is linked to each of them.
  • An app build that predates this feature omits the role field when saving a profile, so the server clears every link and the profile silently becomes a draft, with no warning to the operator.

Validation

  • ./gradlew :common:compileKotlinDesktop
  • ./gradlew :server:testClasses and ./gradlew :server:test — 2433 tests, 1 skipped, 0 failures
  • ./gradlew :worker:testClasses and ./gradlew :worker:test — 810 tests, 4 skipped, 0 failures
  • ./gradlew :app:desktopMainClasses, ./gradlew :app:desktopTestClasses and ./gradlew :app:desktopTest — 1133 tests across 126 classes, 2 skipped, 0 failures

The skips are pre-existing and platform-gated, none introduced by this change. The independent code review covered the credential-linking feature; the agent-role picker consistency change was verified by manual testing and did not receive a separate independent review.

Risks and follow-up

  • Accepted open findings, none of which block this release: link rows rely on the cascade rather than an explicit delete on profile deletion; the offending role ids in the validation error are emitted in set order rather than sorted; editing a profile whose linked role was deleted after the settings list loaded submits a rejected id until the surface is reloaded; there is no Compose UI test for the role chips or the unusable-until-linked marker; the contract uses Set<Long> where the design document said List<Long>; and the delete response returns an empty link set without a comment explaining why.
  • The accepted trade-off behind these: a stale worker cache after a role delete is not pushed, and is corrected by the next bootstrap, which rejects the profile as a signature mismatch.
## Summary Adds a per-agent-role authorization dimension to credential profiles on top of the existing per-worker one. A profile's link set is stored in a new join table, travels inside the app-signed profile body, is relayed to the worker, and is enforced when a terminal session attaches the profile, so only a linked role may use it. The app gains a role multi-select in the profile editor and shows each profile's linked roles. ## Changes - Added `agentRoleIds` to the credential profile DTO and to both write requests, as part of the signed body. - Added the `V38` migration and the `(profile_id, agent_role_id)` join table with cascades on both keys, inserting no rows. - Validated supplied role ids against the caller's own roles and wrote the link set atomically with the profile and its signature. - Scoped the `credential_profiles` prompt section to the intersection of worker and role link. - Split the process-scoped tool environment from the per-call execution context so the acting role reaches the terminal tool only. - Enforced the link in `attach`, refusing a role-less or unlinked caller before any decryption, and filtered the unknown-profile hint accordingly. - Signed the session's role into the built-in tool authorization from the app. - Added the role picker, the linked-role labels and the unusable-until-linked marker to the credential settings surface. - Grouped the credential-profile role picker by project with section headers, matching the spawnable-agent-role picker in the agent-roles tab. - Extracted the chip picker and the role label into one shared implementation used by both dialogs, so the two surfaces cannot drift apart in grouping or wording. ## Implementation notes - The link set is unordered by design: the composite primary key makes duplicates impossible, both sides compare it as a set, and a save is a full replacement. - The worker rejects an attach with no role id before it even resolves the profile name, so a role-less caller cannot learn which profiles a worker holds; an unlinked role is rejected before the enabled flag is consulted. - Link rows are removed by the foreign-key cascade rather than by an explicit delete, which is correct on every connection the server opens because it enforces foreign keys. - In the grouped picker the project is carried by the section header, so chips are labelled `name` or `name — displayName`; profile-card labels keep their `— project` suffix because a card row has no header to carry that context. - The credential picker and the profile-card labels are both derived from the same role and project streams in one place, so a card cannot contradict the picker. ## Deployment - Server, worker and app must be upgraded together; a partially upgraded deployment is unsafe in both directions. - A new worker with an old app denies every credential attach, because the old app signs no role. - An old worker with a new app silently loses the role enforcement entirely, because it ignores the field. - Existing credential profiles become unusable until at least one agent role is linked to each of them. - An app build that predates this feature omits the role field when saving a profile, so the server clears every link and the profile silently becomes a draft, with no warning to the operator. ## Validation - `./gradlew :common:compileKotlinDesktop` - `./gradlew :server:testClasses` and `./gradlew :server:test` — 2433 tests, 1 skipped, 0 failures - `./gradlew :worker:testClasses` and `./gradlew :worker:test` — 810 tests, 4 skipped, 0 failures - `./gradlew :app:desktopMainClasses`, `./gradlew :app:desktopTestClasses` and `./gradlew :app:desktopTest` — 1133 tests across 126 classes, 2 skipped, 0 failures The skips are pre-existing and platform-gated, none introduced by this change. The independent code review covered the credential-linking feature; the agent-role picker consistency change was verified by manual testing and did not receive a separate independent review. ## Risks and follow-up - Accepted open findings, none of which block this release: link rows rely on the cascade rather than an explicit delete on profile deletion; the offending role ids in the validation error are emitted in set order rather than sorted; editing a profile whose linked role was deleted after the settings list loaded submits a rejected id until the surface is reloaded; there is no Compose UI test for the role chips or the unusable-until-linked marker; the contract uses `Set<Long>` where the design document said `List<Long>`; and the delete response returns an empty link set without a comment explaining why. - The accepted trade-off behind these: a stale worker cache after a role delete is not pushed, and is corrected by the next bootstrap, which rejects the profile as a signature mismatch.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Torvian.eu/chatbot!352
No description provided.