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
2 changes: 1 addition & 1 deletion docs/memory/architecture/mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ FastMCP, Streamable HTTP transport, port 8080. API-key auth via `Authorization:

| Module | Tools | Description |
|--------|-------|-------------|
| `agents.ts` (22) | `list_agents`, `get_agent`, `get_agent_info`, `get_agent_compatibility_report`, `create_agent`, `rename_agent`, `delete_agent`, `start_agent`, `stop_agent`, `list_templates`, `get_credential_status`, `inject_credentials`, `export_credentials`, `import_credentials`, `get_credential_encryption_key`, `get_agent_ssh_access`, `deploy_local_agent`, `initialize_github_sync`, `get_agent_github_pat_status`, `set_agent_github_pat`, `export_agent_data`, `import_agent_data` | Agent lifecycle, credentials, SSH, local deploy (#2060: `deploy_local_agent` requires an embedded `.trinity-manifest.json` — set in tool code — verified server-side; deterministic Idempotency-Key; description states the token-bound payload ceiling and directs large agents at the `trinity` CLI), GitHub sync, per-agent PAT (#347), runtime-data export/import (#1169), compatibility report (#668) |
| `agents.ts` (22) | `list_agents`, `get_agent`, `get_agent_info`, `get_agent_compatibility_report`, `create_agent`, `rename_agent`, `delete_agent`, `start_agent`, `stop_agent`, `list_templates`, `get_credential_status`, `inject_credentials`, `export_credentials`, `import_credentials`, `get_credential_encryption_key`, `get_agent_ssh_access`, `deploy_local_agent`, `initialize_github_sync`, `get_agent_github_pat_status`, `set_agent_github_pat`, `export_agent_data`, `import_agent_data` | Agent lifecycle, credentials, SSH, local deploy (#2060: `deploy_local_agent` requires an embedded `.trinity-manifest.json` — set in tool code — verified server-side; deterministic Idempotency-Key; description states the token-bound payload ceiling and directs large agents at the `trinity` CLI), GitHub sync, per-agent PAT (#347), runtime-data export/import (#1169), compatibility report (#668). `export_credentials` / `import_credentials` / `get_credential_encryption_key` carry a per-tool `canAccess` allow-list `{user, system}` (what their human-only backend gates admit — hidden from agent sessions) and return a backend 403 as a stamped `{success:false, …}` envelope (#3435); the other `ADMIN_ONLY` rows still advertised to agents are a shrink-only baseline in `tool-visibility.test.ts` |
| `chat.ts` (4) | `chat_with_agent`, `get_chat_history`, `get_agent_logs`, `fan_out` | Chat (enforces sharing rules), history, logs; `fan_out` = parallel dispatch of one message to several agents (FANOUT-001). Both sync modes apply `MCP_CHAT_TIMEOUT_MS` (default 25000) — sequential `/chat` (#914) and parallel `/task` (#2661); on abort the client queries `/api/agents/{name}/executions` and returns `{status:"queued_timeout", execution_id, message}` so callers poll instead of duplicate-queueing. **Attribution must be provable, never newest-wins**: `/task` runs N tasks concurrently, so `triggered_by`/`source_mcp_key_id`/window are identical across one caller's rows — the matcher also keys on the call's own `message`, takes its trigger set per call site (`/chat` must NOT accept `self_task`), derives the window from the timeout (a fixed 30s made raising the documented knob a silent kill-switch), and returns **nothing when >1 candidate survives**. A duplicate under the same `Idempotency-Key` is answered from the 409's `execution_id` instead of an opaque `API error (409)`. The recovery read is separately bounded (`MCP_RECOVERY_TIMEOUT_MS`, no 401-reauth) because it spends what is left of the gateway budget. **`fan_out` is the third route (#2670)**, and the one that exceeds the gateway most reliably — a batch runs longer than any single task in it. Its receipt names a `fan_out_id`, not an `execution_id`: N rows share one id, so a single execution id could only name an arbitrary member. Ambiguity is therefore measured on the BATCH (>1 distinct `fan_out_id` ⇒ no receipt), and status is deliberately NOT filtered — by abort time a batch is normally a mix of finished and running rows, so requiring non-terminal ones would drop the batches furthest along. Poll `get_fan_out_result`, which folds `schedule_executions` rather than the idempotency snapshot (that snapshot is written by `complete()`, i.e. only once the batch is over, so it cannot answer what is happening now). **Chain-depth refusal (#2806):** `client.chat`/`task`/`fanOut` turn a 403 carrying `detail.error == "inter_agent_depth_exceeded"` into a typed `DepthRefusal` (`parseDepthRefusal`) instead of throwing; `runAgentChat` (every branch) and `fan_out` return it as `{status: "inter_agent_depth_exceeded", agent, depth, max_depth, retryable: false, message}`. A 403 without the code still throws. **Delegation contract (ent#568):** `src/delegation_contract.ts` is the one text the platform prompt's §Agent Collaboration also carries (byte parity: `tests/unit/test_ent568_delegation_contract.py`); `chat_with_agent` and every `chat_with_<agent>` publish it verbatim, `fan_out` / `send_message` repeat its rule sentence, and each description stays ≤ 2,048 chars because Claude Code cuts MCP tool descriptions there (`CLAUDE_CODE_MAX_MCP_DESCRIPTION_LENGTH`; per-mode detail lives in parameter descriptions, which are not cut). Every `execution_id` receipt a `chat_with_*` caller gets carries `readNotResend` — the MCP server's own `queued_timeout` and 409 replay, and the backend's `accepted`/`queued`/sync-`/task` `queued_timeout` snapshots, whose REST-only "poll GET …" line `runAgentChat` rewrites with a liveness-neutral lead; `fan_out_timeout` keeps its `get_fan_out_result` line. **Parent forwarding (ent#224/#3232):** `resolveReportBack` decides once per call whether `parent_execution_id` goes on the `/task` body (parallel and #946 pull-routed branches; sequential `/chat` never carries one) and which `report_back` fields the result gets. Async dispatches send the platform header turn (#2392) by default; a typed `execution_id="manual"` opts out; a self-task with `inject_result` is excluded; sync parallel is opt-in by a typed id; the header turn wins over a typed id, which is forwarded only if `isWellFormedExecutionId` accepts it. `callerTurn` (the forwarded `X-Trinity-Execution-Id`) is computed separately and never cleared by the opt-out. One `[Report-Back #3232]` log line per call records route, arm, source and verdict. `EXECUTION_ID_PARAM_DESCRIPTION` is the one parameter text shared with `dynamic-agents.ts`. Kill switch `MCP_REPORT_BACK_ENABLED` (default `true`): `createServer` reads it, passes it to `createChatTools`, and returns it so `index.ts` hands it to `startExposedToolsReconciler` (`ReconcilerOptions.reportBackEnabled` is required, so `tsc` fails if a start site forgets it). Second consumer of `parent_execution_id`: on agent images without the #2392 header the backend skill gate reads it as the requester execution (validated there as the caller's own). |
| `schedules.ts` (8) | `list_agent_schedules`, `create_agent_schedule`, `get_agent_schedule`, `update_agent_schedule`, `delete_agent_schedule`, `toggle_agent_schedule`, `trigger_agent_schedule`, `get_schedule_executions` | Schedule CRUD and execution history |
| `executions.ts` (5) | `list_recent_executions`, `get_execution_result`, `get_fan_out_result`, `get_agent_activity_summary`, `search_executions` | Execution queries, async result polling, activity monitoring (MCP-007). `get_execution_result` returns `compact_metadata` parsed (#2958), so a long turn reads as a one-off auto-compaction rather than a degraded agent. `get_fan_out_result` polls a BATCH — `GET /api/agents/{name}/fan-out/{fan_out_id}`, the only surface that answers while it is still running (#2670). `search_executions` (ent#653) proxies the entitlement-gated `/api/enterprise/execution-search` route (`mode=substring` or `mode=regex`, the latter PostgreSQL-only): a per-tool `canAccess` ALLOW-LIST of `system`/`user` scope (agent-scoped sessions never see it, and the backend refuses them regardless) and an honest `available:false` envelope when the module is absent (404) or unentitled (403) — never "no results" |
Expand Down
66 changes: 22 additions & 44 deletions docs/memory/feature-flows/credential-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,26 +183,16 @@ const exportToGit = async () => {

### Backend (`src/backend/routers/credentials.py`)
```python
@router.post("/agents/{agent_name}/credentials/export", response_model=CredentialExportResponse)
async def export_credentials(agent_name: str, current_user: User = Depends(get_current_user)):
"""Export agent credentials to encrypted file in workspace."""
encryption_service = CredentialEncryptionService()

# Read credential files from agent
file_paths = ['.env', '.mcp.json']
files = await encryption_service.read_agent_credential_files(agent_name, file_paths)

# Encrypt and write to agent
encrypted = encryption_service.encrypt(files)
await encryption_service.write_agent_credential_files(
agent_name, {'.credentials.enc': encrypted}
)

return CredentialExportResponse(
status="success",
files_exported=len(files),
encrypted_file=".credentials.enc"
)
@router.post("/agents/{agent_name}/credentials/export")
async def export_credentials(
request: Request,
agent_name: str = Depends(get_owned_agent_by_name), # owner or admin, else uniform 404 (#186); connector -> 403
current_user: User = Depends(get_current_user)
):
reject_agent_principal(current_user) # agent-scoped key -> 403 "human-only" (ent#69 Part 2)
# container missing -> 404; not running -> 400
encrypted_file, files_exported = await get_credential_encryption_service().export_to_agent(agent_name)
# audited (SEC-001); agent server unreachable -> 503
```

### Encryption Service (`src/backend/services/credential_encryption.py`)
Expand Down Expand Up @@ -260,30 +250,16 @@ const importFromGit = async () => {

### Backend (`src/backend/routers/credentials.py`)
```python
@router.post("/agents/{agent_name}/credentials/import", response_model=CredentialImportResponse)
async def import_credentials(agent_name: str, current_user: User = Depends(get_current_user)):
"""Import credentials from encrypted file in agent workspace."""
encryption_service = CredentialEncryptionService()

# Read encrypted file
enc_files = await encryption_service.read_agent_credential_files(
agent_name, ['.credentials.enc']
)

if '.credentials.enc' not in enc_files:
raise HTTPException(status_code=404, detail="No .credentials.enc file found")

# Decrypt
files = encryption_service.decrypt(enc_files['.credentials.enc'])

# Write decrypted files to agent
await encryption_service.write_agent_credential_files(agent_name, files)

return CredentialImportResponse(
status="success",
files_imported=list(files.keys()),
message=f"Imported {len(files)} file(s)"
)
@router.post("/agents/{agent_name}/credentials/import")
async def import_credentials(
request: Request,
agent_name: str = Depends(get_owned_agent_by_name), # owner or admin, else uniform 404 (#186); connector -> 403
current_user: User = Depends(get_current_user)
):
reject_agent_principal(current_user) # agent-scoped key -> 403 "human-only" (ent#69 Part 2)
# container missing -> 404; not running -> 400
files = await get_credential_encryption_service().import_to_agent(agent_name)
# missing .credentials.enc / wrong key -> 400; agent server unreachable -> 503; audited (SEC-001)
```

---
Expand Down Expand Up @@ -330,6 +306,8 @@ async def decrypt_and_inject(request: InternalDecryptInjectRequest):

## MCP Tools

**Who sees which tool (#3435).** `export_credentials`, `import_credentials` and `get_credential_encryption_key` each carry a per-tool `canAccess` allow-list `{user, system}` — exactly the scopes their backend gates admit — so an agent-scoped session neither lists nor can call them (FastMCP answers "not found"). Their descriptions name the caller class (owner or admin for export/import, administrator for the key). A backend 403 comes back as `{success:false, error, human_only|admin_only|not_authorized}` through `accessDenied`, so the audit row reads `denied` (#2807); 400/404/503 are still thrown. No backend gate changed. `inject_credentials` is still advertised to agent sessions (shrink-only baseline in `tool-visibility.test.ts`).

### inject_credentials
```typescript
// src/mcp-server/src/tools/agents.ts
Expand Down
8 changes: 5 additions & 3 deletions docs/user-docs/credentials/credential-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,13 @@ The inject, export, import, checklist, and drift routes are owner-only **and hum

- `get_credential_status(name)` -- Check credential file status.
- `inject_credentials(name, credentials)` -- Inject credentials into the agent.
- `export_credentials(name)` -- Export credentials to encrypted file.
- `import_credentials(name)` -- Import credentials from encrypted file.
- `get_credential_encryption_key()` -- Retrieve the encryption key.
- `export_credentials(name)` -- Export credentials to encrypted file. The agent's owner or an admin only, with a user-scoped or system-scoped key.
- `import_credentials(name)` -- Import credentials from encrypted file. The agent's owner or an admin only, with a user-scoped or system-scoped key.
- `get_credential_encryption_key()` -- Retrieve the encryption key. An administrator only, with a user-scoped or system-scoped key.
- `list_available_credentials()` / `fetch_credential(name)` -- Read a granted vault credential at runtime (see [Credential Vault](#credential-vault)).

`export_credentials`, `import_credentials` and `get_credential_encryption_key` are human-only: an agent-scoped key can never call them, so they are not listed in an agent's MCP session at all. If one of them is refused, the tool returns `{"success": false, "error": "…"}` with `human_only`, `admin_only` or `not_authorized` set, rather than an error string.

## See Also

- [Agent Configuration](../agents/agent-configuration.md)
Expand Down
2 changes: 1 addition & 1 deletion docs/user-docs/faq/credentials-and-subscriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ The encryption key (`CREDENTIAL_ENCRYPTION_KEY`) rotates online, with no downtim

## How do I move an agent's credentials to another Trinity instance?

Export the credentials to a `.credentials.enc` file (**Export to Git** on the Credentials tab, or the `export_credentials` MCP tool), bring that file to the agent's workspace on the target instance, and import it there (**Import from Git** on the Credentials tab, or the `import_credentials` MCP tool). The catch is the encryption key: the file only decrypts if the target instance uses the same `CREDENTIAL_ENCRYPTION_KEY` as the source. An admin can retrieve the key from the source instance (via the encryption-key API endpoint or the `get_credential_encryption_key` MCP tool) and configure it on the target before importing. See [Credential Management](../credentials/credential-management.md).
Export the credentials to a `.credentials.enc` file (**Export to Git** on the Credentials tab, or the `export_credentials` MCP tool), bring that file to the agent's workspace on the target instance, and import it there (**Import from Git** on the Credentials tab, or the `import_credentials` MCP tool). The catch is the encryption key: the file only decrypts if the target instance can decrypt with the source's `CREDENTIAL_ENCRYPTION_KEY`. An admin can retrieve the key from the source instance (via the encryption-key API endpoint or the `get_credential_encryption_key` MCP tool). On a target that already has its own key, do **not** replace it — everything that instance has already encrypted would stop decrypting. Set the source key as `CREDENTIAL_ENCRYPTION_KEY_SECONDARY=<that key>` (a decrypt-only fallback) in the target's `.env` and restart the backend, then import; remove the secondary once the import is done. Only on a brand-new target with nothing encrypted yet can you set it as `CREDENTIAL_ENCRYPTION_KEY` itself. All three MCP tools are human-only: call them with a user-scoped or system-scoped MCP key (the agent's owner or an admin for export and import, an administrator for the key) — an agent's own key can never call them, and they are not listed in an agent's session. See [Credential Management](../credentials/credential-management.md).

## How do I know which credentials an agent actually needs?

Expand Down
10 changes: 7 additions & 3 deletions src/mcp-server/src/access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ const ENT629 =
"abilityai/trinity-enterprise#629 — the backend route is owner-equivalent for an agent key (Invariant #8); MCP gate pending that ruling";
const ADMIN_ONLY =
"backend rejects agent principals (require_admin / assert_admin / reject_agent_principal, #1890)";
const CREDENTIAL_HUMAN_ONLY =
"backend: get_owned_agent_by_name + reject_agent_principal (trinity-enterprise#69 Part 2) — owner or admin, human-only; hidden from agent sessions by a per-tool canAccess (#3435)";
const KEY_ADMIN_ONLY =
"backend require_admin (#1890): admin + human-only; hidden from agent sessions by a per-tool canAccess (#3435)";
const TEARDOWN_HUMAN_ONLY =
"backend fence: the gated route requires role 'creator' AND a HUMAN caller — reject_agent_principal plus a credential-kind refusal, because one call removes N agents without delete_agent's per-agent spawn-scope check (abilityai/trinity-enterprise#454)";
const REMINDER_SELF_GATE = "backend self-gate: reminders.py::_self_gate refuses an agent key naming another agent";
Expand Down Expand Up @@ -257,11 +261,11 @@ export const TOOL_ACCESS_POLICY: Readonly<Record<string, ToolAccessPolicy>> = {
list_templates: { kind: "none", why: "no agent target" },
get_credential_status: { kind: "baselined", owner: ENT629 },
inject_credentials: { kind: "baselined", owner: ADMIN_ONLY },
export_credentials: { kind: "baselined", owner: ADMIN_ONLY },
import_credentials: { kind: "baselined", owner: ADMIN_ONLY },
export_credentials: { kind: "baselined", owner: CREDENTIAL_HUMAN_ONLY },
import_credentials: { kind: "baselined", owner: CREDENTIAL_HUMAN_ONLY },
export_agent_data: { kind: "baselined", owner: ENT629 },
import_agent_data: { kind: "baselined", owner: ENT629 },
get_credential_encryption_key: { kind: "none", why: "no agent target" },
get_credential_encryption_key: { kind: "none", why: "no agent target; " + KEY_ADMIN_ONLY },
get_agent_ssh_access: { kind: "baselined", owner: ADMIN_ONLY },
deploy_local_agent: { kind: "none", why: "creates the agent; `name` is the new agent's name, not a target" },
initialize_github_sync: { kind: "baselined", owner: ENT629 },
Expand Down
Loading
Loading