Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,134 +1,68 @@
# Channel configuration service access recovery
# Channel service selection

Channel service selection and the user's NyxID authorization are separate
decisions. The editor shows the services the current session can select;
Manage service access opens the existing full NyxID consent flow. It never
promises a consent page limited to the services named in a link.
Bind and Edit list active services the user's NyxID account can use, regardless
of which services the user selected at login. The Services header has no Manage
service access action, and the channel form does not redirect into OAuth consent.
This supersedes the former login-grant filtering and consent-return draft flow.

## Link contract
## Source and authorization boundary

Use repeated `requiredServiceId` query parameters on the canonical edit or Bind URL:
The authenticated `GET /api/v1/user-services` inventory owns service identity,
activity and account access. Human-session inventory is independent of OAuth
service selections. The console does not decode `allowed_service_ids` or
`allow_all_services` to constrain channel selection. Personal services and
organization services allowed by membership are selectable when active; NyxID
organization viewer entries (`credential_source.allowed=false`) remain unavailable.

```text
/scopes/:scopeId/channels/:registrationId/edit?requiredServiceId=:userServiceId&requiredServiceId=:anotherUserServiceId
/scopes/:scopeId/channels/bind/:botId?skillId=:optionalSkillId&requiredServiceId=:userServiceId&requiredServiceId=:anotherUserServiceId
```

Each value is the exact NyxID **UserService ID**, obtained from an authoritative
service reference. A catalog ID, display name, slug, channel registration ID,
or Aevatar published-service ID is not interchangeable with this identity.
The editor trims and deduplicates hints, accepts at most 20 nonempty values of
at most 128 characters each, and ignores invalid values. Hints are not grants,
do not select a service automatically, and do not become channel requirements.

Names and slugs come from the authenticated NyxID user-service inventory.
Effective availability requires an active service, account access, and the
current bearer grant for that exact ID (or an explicit all-services grant).
Another service with the same slug cannot satisfy the hint. Missing inventory
entries are shown as unresolved services with the requested ID behind a
details disclosure; unavailable services are not presented as selectable.

## User path

1. The user opens the edit or Bind link. Services lists the missing requested access
above the existing searchable channel selection.
2. Manage service access saves the non-secret, unsaved label, skill name and
selected IDs in tab-scoped session storage before leaving. A storage or
redirect failure keeps the editor open with a retryable error.
3. The existing `NyxIDAuthClient` starts `serviceAccessReview` with the complete
configuration path, query and fragment as `returnTo`. It uses the existing PKCE,
callback and backend finalization flow. It supplies no targeted `resource`
or `preselect_service_ids` parameters.
4. NyxID currently displays the full consent page. The editor directs users to
**Customize** under **Service access**, retain services they still need,
select the listed services, and choose **Allow**. Viewing that page does not
imply that any permission was granted.
5. Returning reloads the actual service inventory/grants and restores the
editor draft. New available services are marked Requested but remain
unselected until the user chooses them. Partial or cancelled authorization
leaves the remaining access needs visible. The callback's return action is
labeled Back to previous page because it may return to this editor.
Restored edits receive one short inline reminder to review selections and
save. Do not show a generic permission-check success heading or panel;
only unresolved access needs warrant a separate notice and service list.
6. The user selects services and clicks **Save changes** or **Bind bot** explicitly.
Both actions follow the channel's existing accepted-to-observed confirmation.
Access review itself never saves or binds the channel. After a successful access
refresh, deleted, inactive or unauthorized services disappear from the selection
list and are removed from the draft, selected count and next submission. There
are no Unavailable placeholder rows or manual-deselection warnings. Later
reauthorization makes a service selectable again without restoring its old
selection. Pending or failed access requests never erase draft choices.
Required built-in services still block submission when unavailable. A missing
service explicitly requested by the URL remains in the separate access notice.
Both Bind and Edit submit exact UserService IDs with
`authorization_mode=explicit_service_allowlist`. Saving uses the existing backend
registration authorization planner: it verifies active instances and ownership,
asks NyxID for the Agent Key scope plan, and creates or updates that independent
channel credential. This behavior was checked against `feature/integrate`;
no backend contract change is needed. Account access does not prove credential
health, and the server revalidates each submitted selection.

Both routes reuse `ChannelConfigurationPage`, `ChannelServicePicker` and
`ChannelServiceAccessNotice`, including loading, retry and revoked-selection cleanup
behavior. Bind needs no saved channel registration to review access.
## Link hints and user path

The temporary draft is keyed by account subject, scope and a typed target:
`bind + botId` for an unbound bot, or `edit + registrationId` for a saved channel.
These identities never substitute for each other, even if raw ID strings coincide.
The draft has a one-hour expiry and is cleared after restoration, completed save
or binding, or explicit discard. It contains no credentials and cannot establish
authorization or a saved channel fact. Browser history restoration resets pending
review state and refreshes service access.
Repeated `requiredServiceId` parameters on the canonical Bind or Edit URL remain
advisory hints for exact UserService IDs:

On Bind, a restored skill choice takes precedence over the link's `skillId`
default, including an explicitly cleared choice. Authorization return preserves
the complete link but does not reapply the default or automatically select newly
authorized services. Binding still targets the original bot ID and completes only
after the returned registration ID and bot ID are observed with the submitted
configuration.

## Current NyxID review limitation

The ordinary `prompt=consent` page can initialize from the app's default
services instead of the user's latest saved consent. On 2026-09-29, the live
review page showed the original six services even though the latest Authorized
Apps entry contained the two additionally granted UserService IDs. Both extra
services were available but unchecked under Customize. Merely opening the
review did not remove the saved grant.

The user must include every service they intend to retain before submitting
that ordinary review. Its consent decision replaces the selected service
boundary; the channel's draft selections do not initialize NyxID's picker.
Do not send the consent page's server-generated `preselect_service_ids` as
an invented `/oauth/authorize` contract, or substitute slug-based `resource`
parameters: resource requests can narrow issued authority and cannot reliably
represent distinct same-slug UserServices.
```text
/scopes/:scopeId/channels/:registrationId/edit?requiredServiceId=:userServiceId
/scopes/:scopeId/channels/bind/:botId?skillId=:optionalSkillId&requiredServiceId=:userServiceId
```

[NyxID PR #1683](https://github.com/ChronoAIProject/NyxID/pull/1683) introduces
explicit incremental consent with `service_access_mode=incremental` and exact
repeated `requested_service_ids`. It was open during this investigation.
After the backend and consent UI deploy, Aevatar must integrate that contract
and verify repeated consent preserves the accumulated grant. This full-review
fallback does not claim that capability.
The editor trims and deduplicates hints, accepts at most 20 nonempty values of at
most 128 characters, and ignores invalid values. Hints neither select services
nor add channel requirements. Active matching instances are marked Requested;
a same-slug instance never substitutes for the requested ID. Missing or inactive
hints get an availability notice with unresolved IDs behind a details disclosure.
There are no instructions to customize login consent.

Users search and select services, then explicitly click Bind bot or Save changes.
The optional Skill's backend suggestions use this same inventory boundary.
Required built-in services retain their existing selection and validation rules.
A successful inventory refresh removes missing, inactive or account-denied
selections from the draft, selected count and next submission. Active services
outside login consent remain selected. Failed or pending refreshes preserve edits
and block saving. Reactivation makes services selectable without reselecting them.

There is no channel consent redirect or temporary session-storage draft. Unsaved
navigation retains its ordinary discard protection. Saving still completes only
when the submitted configuration is observed, not when a command is accepted.

## Verification and visual direction

Keep the existing compact white work surface, AlibabaSans typography, blue
actions and token-based amber access notice. Missing services are a short list
with readable names and slugs; the existing search and checkboxes remain the
channel-selection controls. Actions and rows wrap at mobile widths.

Route integration tests exercise exact-ID hints, duplicate slugs, partial
authorization, explicit selection/save, draft restoration, cancellation,
redirect/storage failure and account isolation. Existing adapter and callback
tests protect grant validation and the shared return flow. Browser history
restoration coverage verifies fresh grants, temporary draft cleanup and the
removal of revoked selections from the list, count and submission, while failed
refreshes preserve the draft. Bind route coverage
also verifies the complete return URL, explicit binding after refreshed grants,
accepted-versus-observed completion, restored skill overrides and clearing, and
isolation across bots and edit registrations. Full frontend
typecheck, suite and production build are delegated to GitHub CI.
Keep the compact Channels form, search, selected count, typography and design
tokens. Route integration tests cover Bind and Edit with empty login grants,
exact-instance selection, explicit submission, Bind observation, inactive/deleted
cleanup, failed-refresh preservation and reactivation. Adapter tests cover account
inventory, organization availability, authentication rejection and token refresh.
Full frontend typecheck, suite and production build are delegated to GitHub CI.

Design baseline:
`../../design-baselines/workflow-activity-vnext/`, primary
Design baseline: `../../design-baselines/workflow-activity-vnext/`, primary
`aevatar-workflow-activity-vnext.excalidraw`, SHA-256
`30e74d7b410ae72c4c91432355436679033679c54c10b1702908435b001577de`.
Contract: `2026-08-04-workflow-activity-vnext-design.md`.
User paths: `2026-08-04-workflow-activity-vnext-user-paths.md`.
Existing auth/session/returnTo and Umi localization remain authoritative.
Production data comes from real APIs and acknowledged user actions only.
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Channel Skill service suggestions

Issue: https://github.com/aevatarAI/aevatar/issues/3678

## Backend-owned discovery

The console calls `GET /api/skills/service-recommendations?skillName=...` on
Mainnet. The backend owns Skill resolution, service discovery and evidence.
See the [backend contract](https://github.com/aevatarAI/aevatar/blob/3da66b8813c03a7b1d87616889dd3ff343dcde1a/docs/contracts/skill-service-recommendations.md)
for the response, source contracts, layering and limitations.

The backend is delivered in [PR #3683](https://github.com/aevatarAI/aevatar/pull/3683)
against `feature/integrate`. This console change is delivered separately in
[PR #3679](https://github.com/aevatarAI/aevatar/pull/3679) against
`feat/2026-08-04_workflow-activity-vnext`. The backend endpoint must be merged
and deployed for recommendations to become available.

The browser does not fetch `SKILL.md`, load the catalog for inference, or match
service names against Skill content. Its API adapter validates the selected
Skill identity, evidence enums and exact UserService instance IDs. A missing or
unsupported backend result produces a retryable discovery state, with the
manual picker still usable; there is no local inference fallback.

## User behavior

Binding and editing show suggestions after a Skill is selected. Reasons name the
backend's evidence category without exposing private instructions. Account
instances show personal/organization source, activity and availability.
Active services available to the NyxID account determine which exact IDs are
selectable, including services omitted at login. Account-level organization
restrictions still apply. Login service grants do not constrain a channel
Agent Key selection. Both Bind and Edit use this same inventory boundary; see
[Channel service selection](2026-09-28-channel-service-access.md).

Selecting an instance adds its exact UserService ID to the form; normal save
commits it. Discovery never selects, authorizes or removes services. Changing or
clearing the Skill preserves manual choices. Queries are keyed by scope and
Skill name and consume abort signals, so late results do not replace the current
Skill's recommendations.

Missing connections and inactive services offer NyxID connection management in a
new tab. Channel forms no longer show an OAuth service-access review action or
consent instructions. Refresh reloads backend discovery and account inventory.
Pending and failed discovery stay within the suggestion region.

The current backend sources do not declare mandatory dependencies. Suggestions
remain advisory; text mentions explicitly say they may be needed for some tasks.
An empty result does not prove no services are required. Existing platform-
required services keep their own behavior. Inventory is not credential-validity
evidence.

## Design and verification

Retain the compact Channels form, design tokens, wrapped names, keyboard actions
and English/Chinese locales. Excalidraw assets remain unchanged:
`30e74d7b410ae72c4c91432355436679033679c54c10b1702908435b001577de`.
Use real authenticated backend responses; mocks exist only in tests.

Route integration tests cover rendering backend suggestions, exact instance
selection and save, access gaps, switching/clearing, stale responses and recovery.
API tests cover request encoding and malformed/mismatched results.
Full frontend typecheck, suite and production build are delegated to GitHub CI.
47 changes: 35 additions & 12 deletions apps/aevatar-console-web/src/locales/channelMessages.en-US.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,33 @@
export default {
'channels.suggestions.title': 'Suggested for {skill}',
'channels.suggestions.refresh': 'Refresh suggestions',
'channels.suggestions.loading': 'Finding related services...',
'channels.suggestions.error':
'Could not identify related services. Refresh to retry, or select services manually below.',
'channels.suggestions.help':
'Suggestions may be incomplete and do not confirm required dependencies. Select the services your task needs; each addition is saved with the channel.',
'channels.suggestions.linked': 'Linked to this skill in Ornn.',
'channels.suggestions.catalog': 'The service catalog recommends this skill.',
'channels.suggestions.mention':
'Mentioned in the skill description or instructions; may be needed for some tasks.',
'channels.suggestions.unknownSource': 'Unknown source',
'channels.suggestions.checking': 'Checking access...',
'channels.suggestions.accessError':
'Could not check access. Refresh to retry.',
'channels.suggestions.inactive': 'Inactive — manage this service in NyxID.',
'channels.suggestions.unavailable':
'Access unavailable — check with the service owner.',
'channels.suggestions.missingInstance':
'No active connection found. Refresh or check this service in NyxID.',
'channels.suggestions.notSelected': 'Not selected',
'channels.suggestions.selected': 'Selected',
'channels.suggestions.selectNamed': 'Select {service}',
'channels.suggestions.select': 'Select',
'channels.suggestions.notConnected':
'No connection found in your account. Add this service in NyxID.',
'channels.suggestions.empty':
'No related services identified. You can still select services manually below.',
'channels.suggestions.manage': 'Manage connections in NyxID ↗',
'channels.column.owner': 'Owner',
'channels.owner.organization': 'Organization',
'channels.owner.id': 'Owner ID',
Expand Down Expand Up @@ -82,7 +111,7 @@ export default {
'channels.edit.servicesError':
'Could not load your services. Try again before saving.',
'channels.edit.servicesEmpty':
'No services are available with your current authorization.',
'No active services are available in your NyxID account.',
'channels.edit.missingServices':
'Some saved services are no longer available. Deselect them before saving.',
'channels.edit.selectionError':
Expand Down Expand Up @@ -202,12 +231,12 @@ export default {
'channels.connect.selectAll': 'Select all',
'channels.connect.selectAllResults': 'Select all results',
'channels.connect.servicesHelp':
'Only services available through your current NyxID authorization are shown.',
'Choose active services from your NyxID account, including services you did not select when signing in.',
'channels.connect.servicesLoading': 'Loading services',
'channels.connect.servicesError':
'Could not load your services. Retry before connecting.',
'channels.connect.servicesEmpty':
'No services are available with your current NyxID authorization.',
'No active services are available in your NyxID account.',
'channels.connect.serviceRequired': 'Required',
'channels.connect.requiredServicesMissing':
'Required services unavailable: {services}. Check your NyxID access, then retry.',
Expand Down Expand Up @@ -251,18 +280,12 @@ export default {
'channels.connect.stay': 'Stay',
'channels.connect.discardHelp':
'Your bot token and unsaved choices will be cleared.',
'channels.access.manage': 'Manage service access',
'channels.access.help':
'Missing a service? Click Manage service access to authorize it.',
'channels.access.needed': 'Service access needed',
'channels.access.needed': 'Requested services unavailable',
'channels.access.instructions':
'In NyxID, choose Customize under Service access. Keep the services you still use selected and add the services below, then choose Allow.',
'Check these services in NyxID. Active services your account can use can be selected here.',
'channels.access.unknown': 'Service not found',
'channels.access.requestedIdentity': 'Requested service ID',
'channels.access.unavailable': 'Check availability in NyxID',
'channels.access.notAuthorized': 'Access needed',
'channels.access.restored': 'Your changes have been kept. Review and save.',
'channels.access.startFailed':
'Could not open NyxID. Your changes are still here. Try again.',
'channels.access.notAuthorized': 'Account access unavailable',
'channels.access.requested': 'Requested',
};
Loading
Loading