diff --git a/DEVLOG.md b/DEVLOG.md index c5078de8..65c72639 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -13261,3 +13261,33 @@ repository warnings only); the focused work-session suite passed 8 tests; the full Vitest gate passed 1,316 tests with one intentional live skip; the fresh production build succeeded; and the focused settings Playwright journey passed. + +--- + +## 2026-07-15 — Agent Studio identity, runtime, and client configuration + +Audited the complete agent-configuration journey from fresh desktop and mobile +Playwright captures. Agent identity, workspace binding policy, execution +runtimes, MCP credentials, instance governance, and operational telemetry were +modeled correctly in data but split across ambiguous or duplicated surfaces. + +Renamed the global profile home to Agent Studio and made it the editable source +of truth for identity and the single primary execution runtime. Profile detail +now reports runtime, binding, and MCP-client readiness, aggregates every linked +client by workspace, and deep-links into client creation with the correct +binding selected. Profile identity/execution edits synchronize to active +bindings without overwriting workspace-local policy. + +Made the workspace Agent roster explicitly policy-only, collapsed dense policy +rows by default, and repaired the mobile action layout. Consolidated persistent +client credentials, personal tokens, session keys, scopes, rotation, and +revocation under Agent access; removed Developer clients from navigation and +redirected its legacy routes. Instance Administration remains governance and +Mission Control remains read-only operations. + +Verification: lint and typecheck passed; the canonical serial Vitest gate +passed 1,318 tests with one intentional live-connector skip; and the fresh +production Playwright build passed all 45 desktop, mobile, accessibility, and +application journeys. Before/after evidence plus the UX health report are +preserved under `docs/audits/agent-studio-2026-07-15/`. Production verification +follows after tagging and deployment. diff --git a/docs/agents/overview.md b/docs/agents/overview.md index 07089fa9..df63aad6 100644 --- a/docs/agents/overview.md +++ b/docs/agents/overview.md @@ -7,18 +7,25 @@ This page covers the data model, the human/agent dual-assignment story, the lifecycle of an agent, the implicit heartbeat that comes from webhook delivery, and where to find each piece of the surface in the app. +::: info Configuration map +Use **Agent Studio** for identity, prompt, and the single primary execution +runtime; **Workspace → Agent roster** for binding policy; **Workspace → Agent +access** for one or more MCP client credentials; **Instance Administration** +for global governance; and **Mission Control** for read-only operations. +::: + ## Two ways to run agent work There are two ways to put an agent to work, and they're intentionally different. Reach for the one that matches how much you want to drive. -| | **Direct dispatch** | **Goal orchestration** | -|---|---|---| -| Entry point | Assign / @-mention / queue an **issue** | State a **Goal** → approve a plan | -| Who decides the steps | You do — one issue, one unit of work | A planner agent decomposes into a step DAG | -| Who drives | You — assign, nudge, reassign | An automated crew loop (plan → work → judge → retry) | -| Stops when | The agent finishes that issue | Every step passes, or a budget/time cap trips | -| Reach for it when | You know the task and want an agent on it now | The objective is big and you want it broken down and run to completion on its own | +| | **Direct dispatch** | **Goal orchestration** | +| --------------------- | --------------------------------------------- | --------------------------------------------------------------------------------- | +| Entry point | Assign / @-mention / queue an **issue** | State a **Goal** → approve a plan | +| Who decides the steps | You do — one issue, one unit of work | A planner agent decomposes into a step DAG | +| Who drives | You — assign, nudge, reassign | An automated crew loop (plan → work → judge → retry) | +| Stops when | The agent finishes that issue | Every step passes, or a budget/time cap trips | +| Reach for it when | You know the task and want an agent on it now | The objective is big and you want it broken down and run to completion on its own | **Direct dispatch** is the everyday path: assign an agent to an issue (or @-mention one, or let auto-dispatch pick from the queue) and it works that one @@ -34,8 +41,8 @@ direct assignment or a Goal step, it shows up the same way in Mission Control, counts toward the agent's load, and is watched by the same stalled-run / watchdog logic. A Goal step can also be **materialized into a real issue** (see the orchestration guide), so planned work appears on the board and sprint -alongside everything else. The two paths are different ways to *start* work, not -two different systems for *tracking* it. +alongside everything else. The two paths are different ways to _start_ work, not +two different systems for _tracking_ it. Orthogonal to the path are two other dials, both also carried on the run: the [**engagement mode**](/agents/engagement-modes.html) (how far to take the @@ -74,26 +81,26 @@ model Agent { The columns worth knowing in detail: -| Column | Purpose | -| --- | --- | -| `id` | Internal identifier. Use this to reference an agent from API keys, dispatch rules, or issue assignments. | -| `workspaceId` | Tenant scope. An agent never crosses workspaces. | -| `name` | Human-facing display name. Free to change. | -| `profileKey` | Stable cross-system handle, unique per workspace. Matches the Hermes profile directory name (e.g. `victor`, `mizu`). Treat this like a username — addressable, mostly immutable. | -| `description` | Free-form prose used in the agent picker and on the agent page. | -| `avatar` | Optional URL or data-URI; falls back to the agent's initials. | -| `provider` | Runtime family: `HERMES`, `CLAUDE`, `CODEX`, or `CUSTOM`. Hermes is first-class; Claude/Codex are supported as MCP clients today. | -| `runtimeMode` | `PERSISTENT` or `EPHEMERAL`. Hermes/custom bridges can be persistent; Claude and Codex are currently single-session, with persistent runners on the roadmap. | -| `webhookUrl` | Where Forge POSTs assignment payloads. May also be the synthetic `agent:dispatch:{agentId}` shim — see [Activity & Audit](/concepts/activity-and-audit.html). | -| `webhookSecret` | Per-agent HMAC secret. If unset, Forge falls back to the workspace synthetic secret. | -| `capabilities` | Free-form lowercase tags consumed by `PRIORITY_MATCH` and `CAPABILITY_MATCH` dispatch. Use whatever vocabulary your agents announce. | -| `role` | `WORKER` (default), `COACH`, or `OBSERVER`. See [Roles](#roles). | -| `templateMarkdown` | Optional. When the agent is assigned an issue with an empty description, Forge prepends this content. | -| `status` | `ONLINE`, `OFFLINE`, or `BUSY`. Drives dispatcher eligibility. | -| `lastHeartbeatAt` | Updated automatically on every successful webhook delivery. | -| `maxConcurrent` | Cap on active issues; `0` means unlimited. | -| `lastDispatchedAt` | Round-robin bookkeeping. Null sorts first, so brand-new agents pick up work immediately. | -| `archivedAt` | When set, the agent is hidden from pickers and ineligible for auto-dispatch. | +| Column | Purpose | +| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `id` | Internal identifier. Use this to reference an agent from API keys, dispatch rules, or issue assignments. | +| `workspaceId` | Tenant scope. An agent never crosses workspaces. | +| `name` | Human-facing display name. Free to change. | +| `profileKey` | Stable cross-system handle, unique per workspace. Matches the Hermes profile directory name (e.g. `victor`, `mizu`). Treat this like a username — addressable, mostly immutable. | +| `description` | Free-form prose used in the agent picker and on the agent page. | +| `avatar` | Optional URL or data-URI; falls back to the agent's initials. | +| `provider` | Runtime family: `HERMES`, `CLAUDE`, `CODEX`, or `CUSTOM`. Hermes is first-class; Claude/Codex are supported as MCP clients today. | +| `runtimeMode` | `PERSISTENT` or `EPHEMERAL`. Hermes/custom bridges can be persistent; Claude and Codex are currently single-session, with persistent runners on the roadmap. | +| `webhookUrl` | Where Forge POSTs assignment payloads. May also be the synthetic `agent:dispatch:{agentId}` shim — see [Activity & Audit](/concepts/activity-and-audit.html). | +| `webhookSecret` | Per-agent HMAC secret. If unset, Forge falls back to the workspace synthetic secret. | +| `capabilities` | Free-form lowercase tags consumed by `PRIORITY_MATCH` and `CAPABILITY_MATCH` dispatch. Use whatever vocabulary your agents announce. | +| `role` | `WORKER` (default), `COACH`, or `OBSERVER`. See [Roles](#roles). | +| `templateMarkdown` | Optional. When the agent is assigned an issue with an empty description, Forge prepends this content. | +| `status` | `ONLINE`, `OFFLINE`, or `BUSY`. Drives dispatcher eligibility. | +| `lastHeartbeatAt` | Updated automatically on every successful webhook delivery. | +| `maxConcurrent` | Cap on active issues; `0` means unlimited. | +| `lastDispatchedAt` | Round-robin bookkeeping. Null sorts first, so brand-new agents pick up work immediately. | +| `archivedAt` | When set, the agent is hidden from pickers and ineligible for auto-dispatch. | ::: info Agents are not users. They never log in, never own a session, and they don't @@ -156,23 +163,23 @@ opening each agent's detail page. A typical onboarding sequence: -1. **Choose the provider.** Settings → Agents → New starts with Hermes, - Claude, Codex, or custom. Hermes is the persistent first-class path; - Claude/Codex are single-session MCP clients today. -2. **Create the agent.** Set `name`, `profileKey`, `description`, avatar, - provider, and runtime mode. The agent starts at `status = OFFLINE`. -3. **Pick the connection mode.** MCP-only agents pull work and heartbeat with - a linked API key. Push agents additionally configure a real `webhookUrl` - plus optional `webhookSecret`. +1. **Define the identity.** Agent Studio → New profile starts with Hermes, + Claude, Codex, or custom. Set `name`, `profileKey`, description, avatar, + provider, and execution engine once. +2. **Choose the primary runtime.** A profile can select zero or one execution + host. Active workspace bindings inherit later identity/execution edits. +3. **Bind it to a workspace.** The workspace roster owns local capacity, + routing eligibility, engagement, approval, and capability overrides. 4. **Declare capabilities.** Lowercase, free-form. Common entries match priority names (`urgent`, `high`) for `PRIORITY_MATCH` and label names (`infra`, `frontend`) for `CAPABILITY_MATCH`. 5. **Set the role.** Default `WORKER`. Use `COACH` for an agent that posts diagnostic comments via [AI Coach](/agents/ai-triage-and-coach.html); use `OBSERVER` for an agent that should never auto-pick up work. -6. **Issue a linked MCP key.** Onboarding can create one immediately, or you - can create it later under Developer access. A linked key makes - `agents.me`, `agents.heartbeat`, and `issues.assigned` self-aware. +6. **Add MCP clients.** In Workspace → Agent access, issue one or more linked + credentials for Codex, Claude, Hermes, or another MCP client. A linked key + makes `agents.me`, `agents.heartbeat`, and `issues.assigned` self-aware + without replacing the primary execution runtime. 7. **Bring it online.** Either call `agents.heartbeat`, flip `status` to `ONLINE`, or fire a webhook delivery — the first successful POST will flip OFFLINE → ONLINE automatically via `recordAgentReachable`. @@ -284,11 +291,11 @@ Once an agent is in flight on an issue, the operator can intervene without waiting for the agent to notice. Each `AgentRun` carries a `controlState` (`AgentRunControlState`): -| State | Meaning | -|---|---| -| `NONE` | Steady state. The run is proceeding normally. | -| `PAUSE_REQUESTED` | Operator asked the agent to pause on its next loop tick. | -| `CANCEL_REQUESTED` | Operator asked the agent to abort the run. | +| State | Meaning | +| ------------------ | -------------------------------------------------------- | +| `NONE` | Steady state. The run is proceeding normally. | +| `PAUSE_REQUESTED` | Operator asked the agent to pause on its next loop tick. | +| `CANCEL_REQUESTED` | Operator asked the agent to abort the run. | Three procs write the state: @@ -326,17 +333,17 @@ pipeline row, with options for pause / cancel / redirect-to-… Forge tracks two distinct flavours of "stalled" — the difference matters because the right response is different: -| Flavour | Definition | Where it surfaces | -|---|---|---| -| **Stalled run** | An `AgentRun` is still `ACTIVE` but `lastEventAt` is older than `STALE_RUN_MS` (5 min). Defined once in `src/lib/agent-stale.ts`; consumed by the Mission Control overlay, the agent detail page, the dashboard tile, and the `agentRun.kick` mutation. | Mission Control "Needs attention" lane, agent detail Stalled bucket, dashboard Agents tile (red `N stl` chip), Mission Control glance roster (per-agent red `N stl` pill). | -| **Stalled issue** | An issue is in `IN_PROGRESS` / `IN_REVIEW` and `updatedAt` is older than `Workspace.stalledThresholdDays` (default 7d, settings-driven, `0` disables). | Inbox "Stalled" sub-buckets (yours / agents), dashboard "Stalled" column, agent detail Stalled bucket, dashboard Agents tile (warning `N quiet` chip). | +| Flavour | Definition | Where it surfaces | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Stalled run** | An `AgentRun` is still `ACTIVE` but `lastEventAt` is older than `STALE_RUN_MS` (5 min). Defined once in `src/lib/agent-stale.ts`; consumed by the Mission Control overlay, the agent detail page, the dashboard tile, and the `agentRun.kick` mutation. | Mission Control "Needs attention" lane, agent detail Stalled bucket, dashboard Agents tile (red `N stl` chip), Mission Control glance roster (per-agent red `N stl` pill). | +| **Stalled issue** | An issue is in `IN_PROGRESS` / `IN_REVIEW` and `updatedAt` is older than `Workspace.stalledThresholdDays` (default 7d, settings-driven, `0` disables). | Inbox "Stalled" sub-buckets (yours / agents), dashboard "Stalled" column, agent detail Stalled bucket, dashboard Agents tile (warning `N quiet` chip). | -A stalled *run* is usually a runtime glitch — the agent's last loop tick +A stalled _run_ is usually a runtime glitch — the agent's last loop tick crashed, the webhook didn't deliver, the runner is offline. The operator's first move is the **Kick** button (`agentRun.kick`), which re-fires the dispatch webhook without changing assignment. -A stalled *issue* is usually a design / dependency wait — the agent did +A stalled _issue_ is usually a design / dependency wait — the agent did its turn and is now waiting on a human review, an external dependency, or a clarifying answer in the comment thread. There is no "Kick" for issues; the right move is to read the thread, comment, and possibly @@ -344,7 +351,7 @@ reassign. The agent detail page's Stalled bucket renders both lists side-by-side. When the same issue appears in both (an issue is past the day-threshold -*and* its run is past the 5-minute threshold), the issue row carries an +_and_ its run is past the 5-minute threshold), the issue row carries an "also stalled run" tag so the operator doesn't read the same incident twice. @@ -374,9 +381,9 @@ without leaving the current page. - [Orchestration loop](/concepts/orchestration.html) — the second way to run agent work: Goal → plan → crew loop, and how its steps open runs and can become issues. -- [Engagement modes](/agents/engagement-modes.html) — *how far* a dispatch +- [Engagement modes](/agents/engagement-modes.html) — _how far_ a dispatch takes the work (execute / research / review / discuss). -- [Chat & Dispatch engines](/agents/engines.html) — *who owns the loop* for a +- [Chat & Dispatch engines](/agents/engines.html) — _who owns the loop_ for a run (Forge-owned Completions vs. runtime-owned Runs). - [Hermes Integration](/agents/hermes.html) — how `profileKey` maps to a Hermes runtime profile, and what the MCP self-management loop looks like. diff --git a/docs/agents/profiles-and-bindings.md b/docs/agents/profiles-and-bindings.md index 02412391..d949269b 100644 --- a/docs/agents/profiles-and-bindings.md +++ b/docs/agents/profiles-and-bindings.md @@ -5,7 +5,7 @@ An agent in Forge isn't a single row. It's a **three-tier** model: 1. A global **profile** — the agent's definition (who it is, what it can do), owned by a user, independent of any workspace. 2. A per-workspace **binding** — a workspace adopting that profile, - with its own *policy* (capacity, capability overrides, dispatch + with its own _policy_ (capacity, capability overrides, dispatch eligibility, engagement mode, approval gate). 3. **Instance policy** — the instance admin's governance layer: sharing a profile to every workspace, or force-disabling it. @@ -17,31 +17,37 @@ identity everywhere. ## Tier 1 — Profiles (the definition) A **profile** (`AgentProfile`) is the source of truth for an agent's -identity: its `profileKey` (the stable cross-system handle, matching -the Hermes profile directory name), name, avatar, `provider`, -`runEngine`, default `runtimeId`, `baseCapabilities`, role, and +identity and execution: its `profileKey` (the stable cross-system handle, +matching the Hermes profile directory name), name, avatar, `provider`, +`runEngine`, zero-or-one primary `runtimeId`, `baseCapabilities`, role, and template. It's owned by a user and lives outside any workspace. `profileKey` is **unique per owner**, so two different users can each own a `victor` without colliding. -Manage profiles at **`/settings/agents`** (the global, account-level -agents page). This is also where your profiles surface on +Manage profiles in **Agent Studio** at **`/settings/agents`** (the global, +account-level agents page). Its detail view reports runtime, workspace-binding, +and MCP-client readiness and is the only place identity/execution fields are +edited. This is also where your profiles surface on [Mission Control](/guide/mission-control.html#my-agents). +Updating a profile synchronizes its identity and execution fields into every +active workspace binding. Binding policy such as capacity and capability +overrides is deliberately preserved. + ## Tier 2 — Bindings (the policy) A **binding** is an `Agent` row: a workspace adopting a profile via `Agent.profileId`. Binding copies the profile's definition into the workspace, then layers per-workspace **policy** on top: -| Binding policy | Column | What it does | -|---|---|---| -| Capacity | `maxConcurrent` | Cap on simultaneously-claimed issues (0 = unlimited) | -| Capability overrides | `capabilities` | Per-workspace tags (default: the profile's `baseCapabilities`) — feed `CAPABILITY_MATCH` | -| Auto-dispatch eligibility | `autoDispatchEligible` | When false, never auto-picked here — manual assignment only | -| Engagement mode | `engagementMode` | Per-binding default; null = inherit `Workspace.assignmentEngagementMode` | -| Approval gate | `requireApprovalBeforeStart` | Per-binding override of the workspace approval gate | +| Binding policy | Column | What it does | +| ------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------- | +| Capacity | `maxConcurrent` | Cap on simultaneously-claimed issues (0 = unlimited) | +| Capability overrides | `capabilities` | Per-workspace tags (default: the profile's `baseCapabilities`) — feed `CAPABILITY_MATCH` | +| Auto-dispatch eligibility | `autoDispatchEligible` | When false, never auto-picked here — manual assignment only | +| Engagement mode | `engagementMode` | Per-binding default; null = inherit `Workspace.assignmentEngagementMode` | +| Approval gate | `requireApprovalBeforeStart` | Per-binding override of the workspace approval gate | Bind, set policy, and unbind from the workspace agents page at **`/w/[slug]/settings/agents`**. The **catalog** there lists the @@ -52,7 +58,7 @@ and `remove` — are workspace-admin-gated. ::: tip Unbind vs Delete **Unbind** archives the binding (reversible) — runs, chats, and history are preserved, and re-binding the same profile reuses the archived row. -**Delete** (the trash action beside Unbind) is a *smart remove*: it +**Delete** (the trash action beside Unbind) is a _smart remove_: it hard-deletes a genuinely unused agent (no runs, comments, keys, or assignments) and otherwise archives it — so a deletion can never cascade away an agent's run history. @@ -63,7 +69,7 @@ away an agent's run history. Two governance levers belong to the instance admin (see [Instance admin](/guide/instance-admin.html)): -- **`instanceShared`** — when set, the profile appears in *every* +- **`instanceShared`** — when set, the profile appears in _every_ workspace's bind-catalog, not just the owner's. This is how a shared fleet agent becomes adoptable org-wide. - **`disabledAt`** — a force-disable. A disabled profile (and all its @@ -77,7 +83,7 @@ Two governance levers belong to the instance admin (see ## Requesting a profile -Defining a *new* profile from scratch is instance-admin-only — but any +Defining a _new_ profile from scratch is instance-admin-only — but any member can **request** one: 1. A member calls `agentProfile.request` (the **Request a profile** @@ -93,14 +99,29 @@ member can **request** one: Profiles created directly by an instance admin skip this — they're pre-approved at creation (`requestedById` stays null). +## MCP clients (the connection credentials) + +MCP clients are **not execution runtimes**. A profile has at most one primary +runtime, while each workspace binding may have any number of linked `ApiKey` +credentials for Codex, Claude, Hermes, or other trusted MCP clients. + +Create, inspect, rotate, revoke, and remove those credentials at +**`/w/[slug]/settings/access`**. Agent Studio aggregates the linked clients by +binding and deep-links into Agent access with the correct workspace agent +preselected. The former `/settings/clients` and +`/w/[slug]/settings/clients` inventory routes redirect to Agent access. + ## How this composes with the rest - **Engagement mode** — a binding's `engagementMode` sets the default - *intent* (Execute / Research / Review / Discuss) for work dispatched + _intent_ (Execute / Research / Review / Discuss) for work dispatched to that agent in that workspace; null inherits the workspace default. See [Engagement modes](/agents/engagement-modes.html). -- **Runtimes** — a profile points at a default `runtimeId` (the compute - host); the binding inherits it. See [Runtimes](/agents/runtimes.html). +- **Runtimes** — a profile points at zero-or-one primary `runtimeId` (the + execution host); every active binding inherits profile execution changes. + See [Runtimes](/agents/runtimes.html). +- **MCP clients** — each binding can hold zero-or-many linked credentials; + these authenticate clients but do not replace the primary execution runtime. - **Auto-dispatch** — `autoDispatchEligible` and the binding's `capabilities` feed the workspace dispatcher; `autoDispatchMode` (which agent gets picked) is a different axis from engagement mode diff --git a/docs/agents/runtimes.md b/docs/agents/runtimes.md index 93bda31c..a51eb1ca 100644 --- a/docs/agents/runtimes.md +++ b/docs/agents/runtimes.md @@ -1,9 +1,16 @@ # Runtimes A **Runtime** is the compute environment that hosts one or more agents. -Where `Agent` describes *who* (a profile, a webhook secret, a runtime -mode) and `runtimeMode` describes *how* the agent stays online, `Runtime` -describes *where the work physically happens*. +Where `Agent` describes _who_ (a profile, a webhook secret, a runtime +mode) and `runtimeMode` describes _how_ the agent stays online, `Runtime` +describes _where the work physically happens_. + +::: info Runtime versus MCP client +An Agent Studio profile selects **zero or one primary execution runtime**. +Codex, Claude, Hermes, and custom MCP clients connect with separate linked +credentials in the workspace's **Agent access** page; a binding can have many +of those clients. Adding an MCP client never changes the profile's runtime. +::: ::: tip Distinct from `runtimeMode` `Agent.runtimeMode` is `PERSISTENT | EPHEMERAL` and lives on the agent @@ -23,11 +30,11 @@ enum RuntimeKind { } ``` -| Kind | Endpoint | Presence model | -|---------------|-----------------|------------------------------------------------------| -| `LOCAL_DAEMON`| no `endpoint` | Daemon subscribes to `/api/plugins/events` SSE and pulls work. | -| `REMOTE_HTTP` | webhook URL | Forge pushes events outbound over HTTPS with HMAC. | -| `CLOUD` | reserved | Not yet implemented. | +| Kind | Endpoint | Presence model | +| -------------- | ------------- | -------------------------------------------------------------- | +| `LOCAL_DAEMON` | no `endpoint` | Daemon subscribes to `/api/plugins/events` SSE and pulls work. | +| `REMOTE_HTTP` | webhook URL | Forge pushes events outbound over HTTPS with HMAC. | +| `CLOUD` | reserved | Not yet implemented. | `Runtime.heartbeatAt` is bumped by the runtime itself (not its agents) — an idle runtime can still be alive while none of its agents have done @@ -53,8 +60,8 @@ columns. `LOCAL_DAEMON` rows. The detail page also shows whether the runtime declares `terminal`, `filesystem`, and `git` access, plus the latest sanitized runtime version/environment metadata when the host reports it. -- **Agent detail page** — small Runtime card that click-throughs to the - runtime detail. +- **Agent Studio profile detail** — readiness summary plus an editable primary + Runtime assignment that synchronizes to active workspace bindings. - **Mission Control agents tab** — compact `RuntimeChip` next to the runtime-mode pill. @@ -182,7 +189,7 @@ reply via `chat.finalizeDraft` — no daemon crash. - **`CHAT_MESSAGE_POSTED` (role=USER)** — reads `body` + `context` directly from the SSE payload, calls `agent.context.bundle({ - threadId })` for thread history + linked-issue summary + +threadId })` for thread history + linked-issue summary + workspace, inlines image / PDF / text attachments via `attachments.getInline`, and spawns the provider adapter with the bundle as system prompt and the user message as a content-block @@ -215,13 +222,14 @@ they observe `autoTransitionedTo` in the payload. ::: ::: warning v1 limitations + - Login takes URL + token directly (no OAuth device-code flow yet). - The AGENT_ASSIGNED loop only auto-runs for `CLAUDE` provider agents; other providers receive a placeholder comment. - If `Workspace.startedStatusId` is not configured, agents or daemons should use `statuses.list({ category: "IN_PROGRESS" })` and transition explicitly when Execute-mode work actually starts. -::: + ::: ## Token usage on AgentRun diff --git a/docs/audits/agent-studio-2026-07-15/README.md b/docs/audits/agent-studio-2026-07-15/README.md new file mode 100644 index 00000000..ceb19775 --- /dev/null +++ b/docs/audits/agent-studio-2026-07-15/README.md @@ -0,0 +1,106 @@ +# Agent Studio configuration audit — 2026-07-15 + +## Outcome + +Forge now presents agent configuration as five explicit, non-overlapping +surfaces: + +- **Agent Studio** owns the durable identity, provider, prompt, base + capabilities, and zero-or-one primary execution runtime. +- **Workspace Agent roster** binds that identity into a workspace and owns only + local capacity, routing eligibility, engagement, approval, and capability + overrides. +- **Workspace Agent access** owns zero-or-many MCP client credentials per + binding, plus personal and session credentials, scopes, rotation, and + revocation. +- **Instance Administration** owns approval, instance sharing, disabling, and + removal. +- **Mission Control** remains read-only operational telemetry for runtime + health, capacity, queues, runs, and attention. + +This makes the core relationship explicit: + +```text +Agent profile +├── primary execution runtime: 0..1 +├── workspace binding: 0..n +│ └── MCP client credentials: 0..n +└── instance governance policy: 1 +``` + +The previous Agent Clients inventory has been retired as an independent +surface. Its routes redirect to Agent access so saved links keep working while +credential creation and credential lifecycle have one source of truth. + +## Evidence + +The audit used a fresh production-mode Playwright build with seeded data at +1440×1000 and 390×844. The accepted current-state captures are in +[`after/`](./after/); the original captures are preserved in +[`before/`](./before/) for direct comparison. + +Key accepted captures: + +- `after/01-global-agent-profiles.png` — Agent Studio inventory +- `after/02-create-agent-profile.png` — creation with primary runtime +- `after/03-agent-profile-detail.png` — readiness and connection model +- `after/04-edit-agent-identity.png` — editable identity and execution +- `after/05-add-mcp-client.png` — deep-linked, preselected MCP client wizard +- `after/07-workspace-agent-bindings.png` — collapsed workspace roster +- `after/08-workspace-agent-policy-expanded.png` — policy on demand +- `after/09-agent-access.png` — consolidated credential lifecycle +- `after/10-instance-agent-policy.png` — instance governance +- `after/11-mission-control.png` — read-only operations +- `after/12`–`14` — mobile Agent Studio, detail, and workspace roster + +## Numbered walkthrough health + +1. **Find the agent definition — healthy.** “Account & identity” no longer + claims shared profiles are personal settings, and Agent Studio explains its + identity/runtime/workspace remit at entry. +2. **Create a profile — healthy.** Creation now includes the primary execution + runtime and explains that MCP clients are configured separately. Required + identity fields and the existing instance-admin gate remain clear. +3. **Understand readiness — healthy.** The profile begins with three explicit + cards for runtime, workspace bindings, and MCP clients. Missing execution or + access is visible without inferring it from an empty field. +4. **Edit identity and execution — healthy.** Name, avatar, description, + provider, run engine, base capabilities, and the single runtime can be + edited together. Identity/execution changes propagate to active workspace + bindings while workspace policy remains local. +5. **Attach multiple clients — healthy.** Each workspace binding aggregates all + linked MCP client credentials and opens Agent access with that binding + preselected. Adding another client never replaces the execution runtime. +6. **Tune workspace behavior — healthy.** Roster rows default to compact status + summaries; policy expands only when requested. The large always-visible + three-step explainer and policy grids no longer dominate the page. +7. **Manage credentials — healthy.** Client creation, scopes, inspection, + rotation, revocation, and deletion now share one Agent access surface. The + separate Developer clients navigation and inventory were removed. +8. **Govern the instance — healthy.** Instance Administration remains the only + place for cross-workspace sharing, approval, disable, and removal decisions. +9. **Operate the fleet — healthy.** Mission Control remains visibly read-only + and reports runtime attention, capacity, runs, queues, presence, and recent + activity without becoming a fourth configuration surface. +10. **Use the flow on mobile — healthy after repair.** Settings navigation stays + collapsed, readiness cards stack, the creation callout reflows, and roster + actions remain in a readable action row instead of narrow text columns. + +## Accessibility and usability notes + +- Existing design tokens, focus-ring utilities, semantic buttons, switch roles, + `aria-expanded`, `aria-current`, and labeled settings search were preserved. +- Progressive disclosure reduces vertical scanning and interaction cost but + keeps all binding actions keyboard reachable. +- Readiness does not rely on color alone: each card includes a label, value, + status icon, and explanatory text. +- The Playwright walkthrough validates visible behavior and responsive layout; + it does not replace a full screen-reader or automated WCAG audit. + +## Verification contract + +The focused router coverage proves that profile edits synchronize identity and +execution fields to active bindings and that a profile aggregates multiple MCP +clients. The Playwright journey exercises desktop and mobile entry, creation, +editing, client deep-linking, collapsed/expanded workspace policy, instance +governance, and Mission Control. diff --git a/docs/audits/agent-studio-2026-07-15/after/01-global-agent-profiles.png b/docs/audits/agent-studio-2026-07-15/after/01-global-agent-profiles.png new file mode 100644 index 00000000..36886a0d Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/after/01-global-agent-profiles.png differ diff --git a/docs/audits/agent-studio-2026-07-15/after/02-create-agent-profile.png b/docs/audits/agent-studio-2026-07-15/after/02-create-agent-profile.png new file mode 100644 index 00000000..5d61c342 Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/after/02-create-agent-profile.png differ diff --git a/docs/audits/agent-studio-2026-07-15/after/03-agent-profile-detail.png b/docs/audits/agent-studio-2026-07-15/after/03-agent-profile-detail.png new file mode 100644 index 00000000..6add3e1c Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/after/03-agent-profile-detail.png differ diff --git a/docs/audits/agent-studio-2026-07-15/after/04-edit-agent-identity.png b/docs/audits/agent-studio-2026-07-15/after/04-edit-agent-identity.png new file mode 100644 index 00000000..f802cb90 Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/after/04-edit-agent-identity.png differ diff --git a/docs/audits/agent-studio-2026-07-15/after/05-add-mcp-client.png b/docs/audits/agent-studio-2026-07-15/after/05-add-mcp-client.png new file mode 100644 index 00000000..1369f875 Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/after/05-add-mcp-client.png differ diff --git a/docs/audits/agent-studio-2026-07-15/after/06-global-runtimes.png b/docs/audits/agent-studio-2026-07-15/after/06-global-runtimes.png new file mode 100644 index 00000000..11c8414c Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/after/06-global-runtimes.png differ diff --git a/docs/audits/agent-studio-2026-07-15/after/07-workspace-agent-bindings.png b/docs/audits/agent-studio-2026-07-15/after/07-workspace-agent-bindings.png new file mode 100644 index 00000000..6d065193 Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/after/07-workspace-agent-bindings.png differ diff --git a/docs/audits/agent-studio-2026-07-15/after/08-workspace-agent-policy-expanded.png b/docs/audits/agent-studio-2026-07-15/after/08-workspace-agent-policy-expanded.png new file mode 100644 index 00000000..5868d04f Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/after/08-workspace-agent-policy-expanded.png differ diff --git a/docs/audits/agent-studio-2026-07-15/after/09-agent-access.png b/docs/audits/agent-studio-2026-07-15/after/09-agent-access.png new file mode 100644 index 00000000..2e0234c7 Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/after/09-agent-access.png differ diff --git a/docs/audits/agent-studio-2026-07-15/after/10-instance-agent-policy.png b/docs/audits/agent-studio-2026-07-15/after/10-instance-agent-policy.png new file mode 100644 index 00000000..cb15a4a3 Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/after/10-instance-agent-policy.png differ diff --git a/docs/audits/agent-studio-2026-07-15/after/11-mission-control.png b/docs/audits/agent-studio-2026-07-15/after/11-mission-control.png new file mode 100644 index 00000000..577ef4dd Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/after/11-mission-control.png differ diff --git a/docs/audits/agent-studio-2026-07-15/after/12-mobile-agent-profiles.png b/docs/audits/agent-studio-2026-07-15/after/12-mobile-agent-profiles.png new file mode 100644 index 00000000..f377e1d0 Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/after/12-mobile-agent-profiles.png differ diff --git a/docs/audits/agent-studio-2026-07-15/after/13-mobile-agent-profile-detail.png b/docs/audits/agent-studio-2026-07-15/after/13-mobile-agent-profile-detail.png new file mode 100644 index 00000000..d089837f Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/after/13-mobile-agent-profile-detail.png differ diff --git a/docs/audits/agent-studio-2026-07-15/after/14-mobile-workspace-agent-bindings.png b/docs/audits/agent-studio-2026-07-15/after/14-mobile-workspace-agent-bindings.png new file mode 100644 index 00000000..bc4ad07d Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/after/14-mobile-workspace-agent-bindings.png differ diff --git a/docs/audits/agent-studio-2026-07-15/before/01-global-agent-profiles.png b/docs/audits/agent-studio-2026-07-15/before/01-global-agent-profiles.png new file mode 100644 index 00000000..77284a6c Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/before/01-global-agent-profiles.png differ diff --git a/docs/audits/agent-studio-2026-07-15/before/02-create-agent-profile.png b/docs/audits/agent-studio-2026-07-15/before/02-create-agent-profile.png new file mode 100644 index 00000000..161291c6 Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/before/02-create-agent-profile.png differ diff --git a/docs/audits/agent-studio-2026-07-15/before/03-agent-profile-detail.png b/docs/audits/agent-studio-2026-07-15/before/03-agent-profile-detail.png new file mode 100644 index 00000000..eafdab7e Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/before/03-agent-profile-detail.png differ diff --git a/docs/audits/agent-studio-2026-07-15/before/04-global-runtimes.png b/docs/audits/agent-studio-2026-07-15/before/04-global-runtimes.png new file mode 100644 index 00000000..0fba30cf Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/before/04-global-runtimes.png differ diff --git a/docs/audits/agent-studio-2026-07-15/before/05-workspace-agent-bindings.png b/docs/audits/agent-studio-2026-07-15/before/05-workspace-agent-bindings.png new file mode 100644 index 00000000..3e13b160 Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/before/05-workspace-agent-bindings.png differ diff --git a/docs/audits/agent-studio-2026-07-15/before/06-agent-clients.png b/docs/audits/agent-studio-2026-07-15/before/06-agent-clients.png new file mode 100644 index 00000000..a8ecc5e4 Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/before/06-agent-clients.png differ diff --git a/docs/audits/agent-studio-2026-07-15/before/07-developer-access.png b/docs/audits/agent-studio-2026-07-15/before/07-developer-access.png new file mode 100644 index 00000000..263d8b2b Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/before/07-developer-access.png differ diff --git a/docs/audits/agent-studio-2026-07-15/before/08-instance-agent-policy.png b/docs/audits/agent-studio-2026-07-15/before/08-instance-agent-policy.png new file mode 100644 index 00000000..cb15a4a3 Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/before/08-instance-agent-policy.png differ diff --git a/docs/audits/agent-studio-2026-07-15/before/09-mission-control.png b/docs/audits/agent-studio-2026-07-15/before/09-mission-control.png new file mode 100644 index 00000000..670a6556 Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/before/09-mission-control.png differ diff --git a/docs/audits/agent-studio-2026-07-15/before/10-mobile-agent-profiles.png b/docs/audits/agent-studio-2026-07-15/before/10-mobile-agent-profiles.png new file mode 100644 index 00000000..518c24fe Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/before/10-mobile-agent-profiles.png differ diff --git a/docs/audits/agent-studio-2026-07-15/before/11-mobile-agent-profile-detail.png b/docs/audits/agent-studio-2026-07-15/before/11-mobile-agent-profile-detail.png new file mode 100644 index 00000000..8bc1cc0b Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/before/11-mobile-agent-profile-detail.png differ diff --git a/docs/audits/agent-studio-2026-07-15/before/12-mobile-workspace-agent-bindings.png b/docs/audits/agent-studio-2026-07-15/before/12-mobile-workspace-agent-bindings.png new file mode 100644 index 00000000..89005317 Binary files /dev/null and b/docs/audits/agent-studio-2026-07-15/before/12-mobile-workspace-agent-bindings.png differ diff --git a/src/app/(app)/settings/access/page.tsx b/src/app/(app)/settings/access/page.tsx index b06a7e70..df7512a7 100644 --- a/src/app/(app)/settings/access/page.tsx +++ b/src/app/(app)/settings/access/page.tsx @@ -1,6 +1,5 @@ "use client"; import { useEffect, useRef, useState, type ComponentType, type ReactNode } from "react"; -import Link from "next/link"; import { useSearchParams } from "next/navigation"; import { toast } from "sonner"; import { @@ -143,7 +142,6 @@ type RevealKeyState = { export default function AccessPage() { const workspace = useMaybeWorkspace(); - const clientsHref = workspace ? `/w/${workspace.slug}/settings/clients` : "/settings/clients"; const searchParams = useSearchParams(); const handledDeepLinkRef = useRef(null); const { data: keys, refetch } = trpc.access.list.useQuery(); @@ -188,7 +186,7 @@ export default function AccessPage() { const baseUrl = typeof window !== "undefined" ? window.location.origin - : process.env.NEXT_PUBLIC_APP_URL ?? "https://forge.axiom-labs.dev"; + : (process.env.NEXT_PUBLIC_APP_URL ?? "https://forge.axiom-labs.dev"); // ── Scope helpers ───────────────────────────────────────────────────────── function applyPreset(p: Preset, setter: (s: Scope[]) => void, presetSetter: (p: Preset) => void) { @@ -198,7 +196,12 @@ export default function AccessPage() { else if (p === "default") setter(DEFAULT_SCOPES); } - function toggleScope(s: Scope, curr: Scope[], setter: (scopes: Scope[]) => void, presetSetter: (p: Preset) => void) { + function toggleScope( + s: Scope, + curr: Scope[], + setter: (scopes: Scope[]) => void, + presetSetter: (p: Preset) => void, + ) { const next = curr.includes(s) ? curr.filter((x) => x !== s) : [...curr, s]; if (setEq(next, FULL_ACCESS)) presetSetter("full"); else if (setEq(next, READ_ONLY)) presetSetter("read"); @@ -339,7 +342,8 @@ export default function AccessPage() { useEffect(() => { const target = searchParams.get("create"); const providerParam = searchParams.get("provider"); - const signature = `${target ?? ""}:${providerParam ?? ""}`; + const requestedAgentId = searchParams.get("agentId"); + const signature = `${target ?? ""}:${providerParam ?? ""}:${requestedAgentId ?? ""}`; if (!target || handledDeepLinkRef.current === signature) return; handledDeepLinkRef.current = signature; @@ -368,7 +372,7 @@ export default function AccessPage() { setScopes(FULL_ACCESS); setPreset("full"); setExpiresInDays(""); - setLinkedAgentId(""); + setLinkedAgentId(requestedAgentId ?? ""); setStep(0); setCreateOpen(true); } @@ -499,37 +503,40 @@ export default function AccessPage() { return ( <> - Register agent + Add MCP client } />
- + {/* ── Registered Agents ── */}
{agentKeys.filter((k) => !k.revokedAt).length} active
} > {agentKeys.map((k) => { - const expired = - !!k.expiresAt && !k.revokedAt && new Date(k.expiresAt) < new Date(); + const expired = !!k.expiresAt && !k.revokedAt && new Date(k.expiresAt) < new Date(); return (
  • @@ -554,9 +561,7 @@ export default function AccessPage() {
    Created {relativeTime(k.createdAt)} .{" "} {k.lastUsedAt ? `used ${relativeTime(k.lastUsedAt)}` : "never used"} - {k.expiresAt && !k.revokedAt && ( - <> . expires {relativeTime(k.expiresAt)} - )} + {k.expiresAt && !k.revokedAt && <> . expires {relativeTime(k.expiresAt)}}
    )} @@ -598,8 +603,7 @@ export default function AccessPage() { > {personalKeys.map((k) => { - const expired = - !!k.expiresAt && !k.revokedAt && new Date(k.expiresAt) < new Date(); + const expired = !!k.expiresAt && !k.revokedAt && new Date(k.expiresAt) < new Date(); return (
  • @@ -611,7 +615,9 @@ export default function AccessPage() { {k.revokedAt && revoked} {expired && expired} {k.projectIds.length > 0 && ( - {k.projectIds.length} project{k.projectIds.length !== 1 ? "s" : ""} + + {k.projectIds.length} project{k.projectIds.length !== 1 ? "s" : ""} + )}
    @@ -622,9 +628,7 @@ export default function AccessPage() {
    Created {relativeTime(k.createdAt)} .{" "} {k.lastUsedAt ? `used ${relativeTime(k.lastUsedAt)}` : "never used"} - {k.expiresAt && !k.revokedAt && ( - <> . expires {relativeTime(k.expiresAt)} - )} + {k.expiresAt && !k.revokedAt && <> . expires {relativeTime(k.expiresAt)}}
    - {sessionKeys.filter((k) => !k.revokedAt && (!k.expiresAt || new Date(k.expiresAt) > new Date())).length} active + { + sessionKeys.filter( + (k) => !k.revokedAt && (!k.expiresAt || new Date(k.expiresAt) > new Date()), + ).length + }{" "} + active
  • @@ -731,8 +740,8 @@ export default function AccessPage() { open={createOpen} onOpenChange={setCreateOpen} size="xl" - title="Register agent key" - description="Select the agent runtime, choose scopes, and copy a provider-specific config after the key is created." + title="Add agent MCP client" + description="Choose the client provider and scopes, then copy a provider-specific MCP config after the key is created." footer={
    @@ -862,8 +871,8 @@ export default function AccessPage() {

    Linked keys let MCP tools such as{" "} - issues.assigned infer the agent - without a profileKey argument. + issues.assigned infer the agent without a{" "} + profileKey argument.

    @@ -878,14 +887,14 @@ export default function AccessPage() { label="Agent link" value={ linkedAgentId - ? agents?.find((a) => a.id === linkedAgentId)?.profileKey ?? "selected" + ? (agents?.find((a) => a.id === linkedAgentId)?.profileKey ?? "selected") : "none" } /> -
    - After creation, Forge will show the raw key once with Hermes, Claude, - Codex, HTTP, and environment config blocks. You can run a same-origin - MCP test before closing the reveal modal. +
    + After creation, Forge will show the raw key once with Hermes, Claude, Codex, HTTP, + and environment config blocks. You can run a same-origin MCP test before closing + the reveal modal.
    )} @@ -947,7 +956,9 @@ export default function AccessPage() { scopes={personalScopes} preset={personalPreset} onApplyPreset={(p) => applyPreset(p, setPersonalScopes, setPersonalPreset)} - onToggleScope={(s) => toggleScope(s, personalScopes, setPersonalScopes, setPersonalPreset)} + onToggleScope={(s) => + toggleScope(s, personalScopes, setPersonalScopes, setPersonalPreset) + } /> @@ -1125,9 +1136,7 @@ export default function AccessPage() {
    {revealKey.linkedAgentId ? ( - @ - {agents?.find((a) => a.id === revealKey.linkedAgentId) - ?.profileKey ?? "agent"} + @{agents?.find((a) => a.id === revealKey.linkedAgentId)?.profileKey ?? "agent"} ) : ( No linked agent @@ -1145,16 +1154,12 @@ export default function AccessPage() {
    Expires
    - {revealKey.expiresAt - ? new Date(revealKey.expiresAt).toLocaleString() - : "Never"} + {revealKey.expiresAt ? new Date(revealKey.expiresAt).toLocaleString() : "Never"}
    Created
    - {revealKey.createdAt - ? new Date(revealKey.createdAt).toLocaleString() - : "just now"} + {revealKey.createdAt ? new Date(revealKey.createdAt).toLocaleString() : "just now"}
    @@ -1204,21 +1209,11 @@ function KeyActions({ {!k.revokedAt && ( <> {showRotate && ( - )} - @@ -1246,12 +1241,14 @@ function ScopeSelector({
    - {([ - { id: "full", label: "Full access" }, - { id: "default", label: "Standard" }, - { id: "read", label: "Read-only" }, - { id: "custom", label: "Custom" }, - ] as { id: Preset; label: string }[]).map((p) => ( + {( + [ + { id: "full", label: "Full access" }, + { id: "default", label: "Standard" }, + { id: "read", label: "Read-only" }, + { id: "custom", label: "Custom" }, + ] as { id: Preset; label: string }[] + ).map((p) => (
    -
    - Use this page to issue or rotate secrets. Use{" "} - - Agent Clients - {" "} - to see active client rows, status, and revocation actions. +
    + This page is the credential source of truth: create, inspect, rotate, revoke, and delete + every MCP client here. Primary execution runtimes stay with the identity in Agent Studio.
    ); @@ -1379,18 +1370,10 @@ function Stepper({ ); } -function ReviewItem({ - label, - value, -}: { - label: string; - value: ReactNode; -}) { +function ReviewItem({ label, value }: { label: string; value: ReactNode }) { return (
    -
    - {label} -
    +
    {label}
    {value}
    ); diff --git a/src/app/(app)/settings/agents/[id]/agent-detail-content.tsx b/src/app/(app)/settings/agents/[id]/agent-detail-content.tsx index 7d156d02..c1da596b 100644 --- a/src/app/(app)/settings/agents/[id]/agent-detail-content.tsx +++ b/src/app/(app)/settings/agents/[id]/agent-detail-content.tsx @@ -2,13 +2,17 @@ import { useState } from "react"; import Link from "next/link"; -import { ArrowLeft, Server } from "lucide-react"; +import { ArrowLeft, CheckCircle2, KeyRound, Link2, Server, TriangleAlert } from "lucide-react"; +import { AgentProvider, RunEngine } from "@prisma/client"; import { toast } from "sonner"; import { Topbar } from "@/components/topbar"; import { Spinner, EmptyState, Section } from "@/components/ui"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { trpc } from "@/lib/trpc"; import { relativeTime } from "@/lib/utils"; import { workspaceChipColor } from "@/components/global-shell/global-shell"; +import { AgentAvatar } from "@/components/agents/agent-avatar"; /** * Client body for the agent profile detail page. Reads @@ -56,14 +60,79 @@ function Def({ label, children }: { label: string; children: React.ReactNode }) ); } +function EditorField({ + label, + hint, + className, + children, +}: { + label: string; + hint?: string; + className?: string; + children: React.ReactNode; +}) { + return ( + + ); +} + +function ReadinessCard({ + icon: Icon, + label, + value, + detail, + ready, +}: { + icon: typeof Server; + label: string; + value: string; + detail: string; + ready: boolean; +}) { + const StateIcon = ready ? CheckCircle2 : TriangleAlert; + return ( +
    +
    + + {label} + +
    +
    {value}
    +

    {detail}

    +
    + ); +} + export function AgentDetailContent({ id }: { id: string }) { const utils = trpc.useUtils(); const [promptDraft, setPromptDraft] = useState(null); + const [definitionDraft, setDefinitionDraft] = useState<{ + name: string; + description: string; + avatar: string; + provider: AgentProvider; + runEngine: "DEFAULT" | RunEngine; + runtimeId: string; + capabilities: string; + } | null>(null); const { data: a, isLoading, error } = trpc.agents.profiles.get.useQuery({ id }); + const { data: runtimes } = trpc.global.runtimes.useQuery(); const updateProfile = trpc.agents.profiles.update.useMutation({ onSuccess: async () => { - toast.success("Agent prompt saved"); + toast.success("Agent profile saved"); setPromptDraft(null); + setDefinitionDraft(null); await utils.agents.profiles.get.invalidate({ id }); await utils.agents.profiles.list.invalidate(); }, @@ -114,13 +183,51 @@ export function AgentDetailContent({ id }: { id: string }) { ? `Your capabilities: ${a.baseCapabilities.join(", ")}.\n\n` : "") + (template ? `${template}\n` : ""); + const clients = a.bindings.flatMap((binding) => + binding.apiKeys.map((client) => ({ ...client, binding, workspace: binding.workspace })), + ); + const activeClients = clients.filter( + (client) => + !client.revokedAt && (!client.expiresAt || new Date(client.expiresAt).getTime() > Date.now()), + ); + const executionReady = !!a.runtime; + const profile = a; + + function beginDefinitionEdit() { + setDefinitionDraft({ + name: profile.name, + description: profile.description ?? "", + avatar: profile.avatar ?? "", + provider: profile.provider, + runEngine: profile.runEngine ?? "DEFAULT", + runtimeId: profile.runtimeId ?? "", + capabilities: profile.baseCapabilities.join(", "), + }); + } + + function saveDefinition() { + if (!definitionDraft) return; + updateProfile.mutate({ + id: profile.id, + name: definitionDraft.name.trim(), + description: definitionDraft.description.trim() || null, + avatar: definitionDraft.avatar.trim() || null, + provider: definitionDraft.provider, + runEngine: definitionDraft.runEngine === "DEFAULT" ? null : definitionDraft.runEngine, + runtimeId: definitionDraft.runtimeId || null, + baseCapabilities: definitionDraft.capabilities + .split(",") + .map((capability) => capability.trim().toLowerCase()) + .filter(Boolean), + }); + } return ( <> - {a.avatar ?? "🤖"} + {a.name}
    -
    +
    +
    + + 0} + /> + 0} + /> +
    + {/* Definition */} -
    -
    - - @{a.profileKey} - - - {a.provider} - {a.runEngine && ( - - {a.runEngine} - - )} - - - {a.runtime ? ( - - - {a.runtime.name} - - ({a.runtime.kind.toLowerCase().replace("_", " ")}) - - - ) : ( - unassigned - )} - - - {a.instanceShared ? ( - Shared with the whole instance +
    + + +
    ) : ( - Private to you - )} - -
    -
    Base capabilities
    -
    - {a.baseCapabilities.length === 0 ? ( - none - ) : ( - a.baseCapabilities.map((c) => ( - - {c} + + ) + ) : undefined + } + > + {definitionDraft ? ( +
    + + + setDefinitionDraft({ ...definitionDraft, name: event.target.value }) + } + /> + + + + setDefinitionDraft({ ...definitionDraft, avatar: event.target.value }) + } + /> + + + + + + + + + + + + + setDefinitionDraft({ + ...definitionDraft, + capabilities: event.target.value, + }) + } + /> + + +