Skip to content
Open
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
6 changes: 3 additions & 3 deletions docs/contracts/nyxid-assistant-conformance/v1/sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"schema_version": 1,
"aevatar": {
"repository": "https://github.com/AevatarAI/aevatar.git",
"revision": "5c59e325c3742bc5a795d0dccda72063a5517067",
"contract_files_sha256": "72f39797c2a5d8106cde3f01e9ee2112088cdf5906b7c3a1ddf571d11d103143",
"revision": "3da66b8813c03a7b1d87616889dd3ff343dcde1a",
"contract_files_sha256": "bc5e5b2b0483421ba62f55a20d55cf889624ac3b0e05ba5fe6bd4de7783830cc",
"files": {
"agents/Aevatar.GAgents.NyxidChat/NyxIdActionPostconditionPort.cs": "7791de469b567dcde70a0f8e2a88cc818972ca557617a2538294e8ccabd5bda0",
"agents/Aevatar.GAgents.NyxidChat/NyxIdAssistantActionRegistry.cs": "60e6f67c94ae11b1bf0dac036ad8ac0c35901e31787b1f0c8173964f6a12d263",
Expand All @@ -17,7 +17,7 @@
"src/Aevatar.AI.ToolProviders.NyxId/NyxIdAssistantToolSource.cs": "e99f2de69d0eb9e0b9dc235e2d568fc66d9dfb79cb0bb1364e01cc210a8c626f",
"src/Aevatar.AI.ToolProviders.NyxId/Tools/NyxIdRequestKeyCreateTool.cs": "2c4f2cda99154f2e667c6cfd291497e697ef11df17f081f96ec70070a8af8b8c",
"src/Aevatar.AI.ToolProviders.NyxId/Tools/NyxIdRequestKeyRotateTool.cs": "18212bb64644cfbca401065bccce439ea5fa00316deff57d730a0d9ac2650e53",
"src/Aevatar.Mainnet.Host.Api/Hosting/MainnetHostBuilderExtensions.cs": "06cf36bdd746d520d3432a0a5a1ea9dbfa88c65af947b4ecf614e32b680f7e6c"
"src/Aevatar.Mainnet.Host.Api/Hosting/MainnetHostBuilderExtensions.cs": "09b74d9ff7056ce0b7d41a15be6ced8f8b99a37eb24d7f3c232fe895bcc7e458"
}
},
"nyxid": {
Expand Down
26 changes: 26 additions & 0 deletions docs/contracts/nyxid-code-execution-conformance/v1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,32 @@ The guard validates whole-file SHA-256 digests and semantic markers for:
- catalog identity propagation, including the no-op same-value transition and customized-row count;
- the `/keys` to `unified_key_service::create_key` credential-validation path.

## Reviewed upstream revision

The current baseline reviews NyxID
[`301fbe732a0f20a3c674184e7ea3408ab62968ab`](https://github.com/ChronoAIProject/NyxID/commit/301fbe732a0f20a3c674184e7ea3408ab62968ab),
including the following changes since `cdd0e3fdad4b45365dc7da3effdb1c1447de8286`:

| Upstream surface | Reviewed behavior and Aevatar impact |
| --- | --- |
| `handlers/api_keys.rs` | Optional conversation information and service-history storage were added. General/scheduled purpose, scheduled-write capability and durable-grant response semantics remain intact; Aevatar's security-class parser still rejects malformed or unexpected authority. |
| `handlers/keys.rs`, `handlers/user_services_handler.rs` | Additive icon, authorship and skill-revision fields do not change the fields consumed by Aevatar. API-key inventory reads now enforce the key's service scope and exclude Viewer organization rows; `/keys` uses read-only listing for API keys. Aevatar consumes caller-visible exact IDs and fails closed for missing/denied routes. It must not infer account-wide absence from scoped inventory. |
| `services/unified_key_service.rs` | `validate_token_exchange_catalog_credential` became `validate_catalog_credential` and also validates IFTTT credentials. The token-exchange branch still calls `provider_token_exchange_service::parse_credential` with the catalog's declared credential fields. The manifest checks the new call and that retained validation path. Other changes concern provisioning eligibility, history and deletion; read-only key listing remains distinct from provisioning. |
| `services/catalog_identity_service.rs` | Storage calls use the service-history collection. Same-value transitions remain no-ops and customized-row accounting still uses matched rows. Identity propagation field semantics are unchanged. |
| `handlers/proxy.rs`, `services/proxy_service.rs` | Proxy changes add destination routing, curation service-account restrictions and billing behavior. Exact instance routing, delegation-token injection and scheduled durable-operation headers remain enforced. Direct and node HTTP bearer forwarding now use `forwarded_caller_token`, which preserves an existing server-owned Authorization header. Its implementation is additionally digest-pinned so the forwarding contract is checked at its new owner. |

The corresponding Aevatar consumers are `NyxIdApiAccessResponseParser`,
`NyxIdApiClient`, `NyxIdCodeExecutionRouteAdmissionPreparer`,
`NyxIdCodeExecutionPort` and `NyxIdDurableCodeExecutionPort`. Their consumed
fields and authorization invariants remain compatible; no production adapter
change is needed for this baseline refresh. Existing focused tests cover
unknown response fields, exact selection, scoped/denied inventory, forwarding
and delegation requirements, and durable headers. Guard self-tests exercise
digest drift, missing markers and unchanged descendant revisions. This is
source-contract review and local regression evidence, not a live deployment test.

## Updating and checking the baseline

`nyxid.reviewed_revision` records the commit at which the hashes were reviewed. Validation does not
require the checkout `HEAD` to equal that commit: `HEAD` may be that revision or a descendant on
`main`. Any tracked source change still fails because its digest changes, so an upstream contract
Expand Down
37 changes: 26 additions & 11 deletions docs/contracts/nyxid-code-execution-conformance/v1/sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
"nyxid": {
"repository": "https://github.com/ChronoAIProject/NyxID.git",
"tracked_ref": "main",
"reviewed_revision": "cdd0e3fdad4b45365dc7da3effdb1c1447de8286"
"reviewed_revision": "301fbe732a0f20a3c674184e7ea3408ab62968ab"
},
"wire_contract": {
"revision": "nyxid-code-execution-wire.v1",
"files": {
"backend/src/handlers/api_keys.rs": "820577ddec0da09cb0ab813c3c8e8e15c6caf2deb9d8667ee5b7e85b5232dc52",
"backend/src/handlers/keys.rs": "188140f2282305fc23ea944885c38e0d66e78edfd44253f05221597c04c761a7",
"backend/src/handlers/proxy.rs": "2320a918a05fce25959f30d5ee8247ae8cca25a8a6eeecef4cc87763f91d10df",
"backend/src/handlers/user_services_handler.rs": "f0afeb1315b581d597e3f076bef35795f967fc014d950ebd56e6dfad20843309",
"backend/src/services/catalog_identity_service.rs": "bf27e10c89a6d419910db20f39fe535d3f8b987e6e1f57fc306ee78a151e6979",
"backend/src/services/unified_key_service.rs": "b75932c6fb74a13cf875a9fe28be3156d2dbfbd8cfdb7bc2b660c9a0aae4da1a"
"backend/src/handlers/api_keys.rs": "3b9bca75b2ae0ad2b1c2e329d73fe91c4f2432d5d70a74749cb4f43c5063154a",
"backend/src/handlers/keys.rs": "217df762b1c1ce3ad3f2e90f2af096a7fc95be7cfb40530c0f3d2d0b5abdf1f4",
"backend/src/handlers/proxy.rs": "7f62c570306be81e4b85ba876107d6d3a6e1149c57af65a626f53274c94d882c",
"backend/src/handlers/user_services_handler.rs": "bd10cdfe9f0e8bbcb561e542beb98d2a947688340ed4094a66c2cb50c2ef99f7",
"backend/src/services/catalog_identity_service.rs": "ad46e5583784041ba358cfdcc1ae86972955e672d6959755a845e6472dc65ef7",
"backend/src/services/proxy_service.rs": "55eff78ffaeb6b18671a247c81180f2b17c9af859d1b25a046b14a53610ae0ca",
"backend/src/services/unified_key_service.rs": "23edabcfae267f7c8c83d32b97c4ae574e123322f21251bd07de40d495c6b06e"
},
"required_markers": {
"backend/src/handlers/api_keys.rs": [
Expand All @@ -28,20 +29,25 @@
"pub auto_connected: bool,",
"pub credential: Option<String>,",
"let credential = body.credential.as_deref().unwrap_or(\"\");",
"unified_key_service::create_key("
"unified_key_service::create_key(",
"unified_key_service::list_keys_read_only_with_grants(",
"crate::services::key_service::ensure_api_key_service_scope("
],
"backend/src/handlers/proxy.rs": [
"if target.service.inject_delegation_token {",
"&target.service.delegation_token_scope,",
"X-NyxID-Durable-Grant-Id is required for scheduled_invocation keys",
"X-NyxID-Operation-Id is required for scheduled_invocation keys",
"if target.service.forward_access_token"
"if target.service.forward_access_token",
"proxy_service::forwarded_caller_token("
],
"backend/src/handlers/user_services_handler.rs": [
"pub catalog_service_id: Option<String>,",
"pub forward_access_token: bool,",
"pub inject_delegation_token: bool,",
"pub delegation_token_scope: String,"
"pub delegation_token_scope: String,",
"let scope = auth_user.api_key_service_scope();",
"scope.is_none_or(|ids| ids.contains(&item.service.id))"
],
"backend/src/services/catalog_identity_service.rs": [
"if changes.is_empty() {",
Expand All @@ -53,7 +59,16 @@
"backend/src/services/unified_key_service.rs": [
"if credential.is_empty()",
"Credential is required for direct routing (or select a node)",
"validate_token_exchange_catalog_credential(&svc, credential)?;"
"validate_catalog_credential(&svc, credential)?;",
"if svc.auth_method != \"token_exchange\"",
"crate::services::provider_token_exchange_service::parse_credential(",
"&exchange_config.credential_fields,"
],
"backend/src/services/proxy_service.rs": [
"pub(crate) fn forwarded_caller_token<'a>(",
"if target.service.forward_access_token",
"name.eq_ignore_ascii_case(\"authorization\")",
"if let Some(token) = forwarded_caller_token(target, caller_token, &extra_outbound_headers) {"
]
},
"forbidden_markers": {
Expand Down
107 changes: 107 additions & 0 deletions docs/contracts/skill-service-recommendations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Skill service recommendations

Skill-to-service discovery is backend behavior shared by all clients. Channels
consumes the result and owns only display, selection and explicit save. Clients
must not download Skill instructions or recreate dependency inference.

The backend implementation targets `feature/integrate`. The Channels consumer
is delivered separately in [console PR #3679](https://github.com/aevatarAI/aevatar/pull/3679),
targeting `feat/2026-08-04_workflow-activity-vnext`. Deploy this endpoint before
enabling the complete recommendation experience in the console. Until then,
the consumer reports discovery as unavailable and preserves manual selection.

## Endpoint

`GET /api/skills/service-recommendations?skillName=<encoded-name>` requires an
authenticated caller and their NyxID bearer credential. It is served by Mainnet
and returns `Cache-Control: no-store`. The name is the same Skill name stored by
channel registration, limited to 128 characters and excluding dot path segments.

```json
{
"skillName": "support",
"suggestions": [{
"slug": "api-github",
"label": "GitHub",
"evidence": "linked",
"instances": [{
"id": "user-service-example",
"slug": "api-github",
"label": "Team GitHub",
"active": true,
"allowed": true,
"source": "organization",
"organizationName": "Example team"
}]
}]
}
```

`evidence` is `linked`, `catalog`, or `mention`. These are advisory sources,
not required/optional dependency declarations. Empty `instances` means no
matching connection in the caller-visible inventory. Restricted Agent Keys may
see only their granted services, so absence is not proof that the account has no
connection. `allowed` reports
account-level access only; it does not grant access to the current session or
channel. `source` is `personal`, `organization`, or `unknown`. Existing
channel authorization still uses explicitly selected exact UserService IDs.
Catalog IDs and Skill association IDs must never become authorization IDs.
Inventory availability is not credential-validity evidence.

Successful empty discovery returns an empty `suggestions` array. Missing
authentication is 401; malformed names return 400 with
`{"code":"invalid_skill_name"}`; inaccessible, malformed, mismatched or
unavailable upstream data returns 502 with
`{"code":"skill_service_discovery_unavailable"}`. Failures never become
successful empty or partial recommendations and never expose upstream errors,
private instructions, file contents, or credentials. Cancellation propagates.

## Ownership and source contracts

- `Aevatar.AI.Abstractions.Skills`: protobuf input/output, evidence and instance
contracts; caller credential parameters remain outside serialized data.
- `Aevatar.AI.Core.Skills.SkillServiceRecommendationService`: stateless discovery
policy behind `ISkillServiceDiscoverySource`. No Host/HTTP dependency.
- `Aevatar.AI.ToolProviders.Ornn.OrnnSkillServiceDiscoverySource`: external
adaptation through existing NyxID/Ornn clients. JSON is decoded at this boundary.
- `Aevatar.Mainnet.Host.Api.Skills.SkillServiceRecommendationEndpoints`:
authentication, HTTP result/error mapping, and response DTOs only.
- Console `channelSkillServicesApi`: response validation only; the UI cannot
invent recommendations or evidence categories.

The adapter resolves the selected name using Ornn `GET /api/v1/skills/:name`,
verifies identity, then loads `GET /api/v1/skills/:guid/json`. It reads the
description, root `SKILL.md`, and optional `nyxidServiceSlug`. It reads NyxID
`GET /api/v1/catalog?include_all=true` and `GET /api/v1/user-services` using
the same invocation's caller credential. Existing source contracts were
compared with `feature/integrate`; no external-product changes are required.
The adapter uses the configured Ornn per-call timeout as the discovery budget,
and rejects root instructions above 200,000 characters.

Evidence priority is explicit Skill association, then exact catalog
`recommended_skills` association, then whole service-name/slug mention in the
description or root instructions. Names shorter than three characters are
excluded from text inference. Catalog and inventory candidates are combined;
all exact same-slug instances remain distinct. Skill instructions are data,
never executed. No LLM inference, recursive Skill loading, grant mutation,
credential creation, or channel update occurs during discovery.

These APIs provide no exhaustive mandatory-dependency contract. Literal matching
can miss aliases or include incidental mentions. Every result is advisory and
may be incomplete. Adding authoritative required dependencies later must extend
the typed contract and source evidence, rather than infer a requirement from
text. Platform-required services retain their current separate contract.

All collections are invocation-local. There is no shared credential/result
cache, actor state, persisted derived dependency list, event replay, projection
priming or query-time materialization. This is transient discovery over external
caller-visible catalog facts, not a new authority for channel configuration.

## Verification

Provider integration tests exercise real policy and adapters against an HTTP
boundary: evidence precedence, bounded name matching, distinct instance IDs,
sanitized failures, identity mismatches, cancellation and caller isolation.
Host tests cover response mapping, authentication, validation, cancellation
forwarding and retryable errors. Console tests cover API decoding, stale-response
isolation, manual selection, exact save IDs and recovery.
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
<Protobuf Include="CodexExecution\codex_execution.proto" GrpcServices="None" />
<Protobuf Include="ToolProviders\agent_tool_admission.proto" GrpcServices="None" />
<Protobuf Include="llm_selection.proto" GrpcServices="None" />
<Protobuf Include="Skills\skill_service_recommendations.proto" GrpcServices="None" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Aevatar.AI.Abstractions.Skills;

// Caller credentials are per invocation, never retained in service state or protobuf data.
public interface ISkillServiceDiscoverySource
{
Task<SkillServiceDiscoveryInput> ReadAsync(
string accessToken, string skillName, CancellationToken ct = default);
}

public interface ISkillServiceRecommendationService
{
Task<SkillServiceRecommendations> RecommendAsync(
string accessToken, string skillName, CancellationToken ct = default);
}

public sealed class SkillServiceDiscoveryException : Exception
{
public SkillServiceDiscoveryException()
: base("Skill service discovery is unavailable.") { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
syntax = "proto3";

package aevatar.ai.skills;
option csharp_namespace = "Aevatar.AI.Abstractions.Skills";

// Advisory discovery evidence; none of these sources declares a mandatory dependency.
enum SkillServiceEvidence {
SKILL_SERVICE_EVIDENCE_UNSPECIFIED = 0;
SKILL_SERVICE_EVIDENCE_LINKED = 1;
SKILL_SERVICE_EVIDENCE_CATALOG = 2;
SKILL_SERVICE_EVIDENCE_MENTION = 3;
}

enum SkillServiceCredentialSource {
SKILL_SERVICE_CREDENTIAL_SOURCE_UNSPECIFIED = 0;
SKILL_SERVICE_CREDENTIAL_SOURCE_PERSONAL = 1;
SKILL_SERVICE_CREDENTIAL_SOURCE_ORGANIZATION = 2;
}

message SkillServiceInstance {
string user_service_id = 1;
string slug = 2;
string label = 3;
bool active = 4;
bool account_access_allowed = 5;
SkillServiceCredentialSource credential_source = 6;
string organization_name = 7;
}

message SkillServiceCatalogEntry {
string slug = 1;
string name = 2;
repeated string recommended_skill_names = 3;
}

// Transient typed input from external adapters. Never persisted or returned to clients.
message SkillServiceDiscoveryInput {
string skill_name = 1;
string description = 2;
string instructions = 3;
string linked_service_slug = 4;
repeated SkillServiceCatalogEntry catalog = 5;
repeated SkillServiceInstance instances = 6;
}

message SkillServiceRecommendation {
string slug = 1;
string label = 2;
SkillServiceEvidence evidence = 3;
repeated SkillServiceInstance instances = 4;
}

message SkillServiceRecommendations {
string skill_name = 1;
repeated SkillServiceRecommendation suggestions = 2;
}
Loading
Loading