Task Breakdown: Connections, scope presets, per-agent grants and the vault
Ordered tasks derived from
plan.md. Each is small enough to land in one PR and ships with tests per Constitution VI. Every schema task ships its migration in the same PR per Constitution V.
Epic ID: AW-15-connections-scopes
Spec: ./spec.md · Plan: ./plan.md
Status: Draft
Last updated: 2026-09-06
How to use
- Tasks are sequential by default.
(parallel)means it may run alongside its predecessor. - Every task names the exact files to create or modify. An implementer should never have to guess a path.
- "Done" is stated explicitly for every task and is checkable without reading the diff.
- Add new tasks at the bottom rather than renumbering.
- Phase boundaries are ship boundaries:
developmust be green and deployable at the end of each phase.
Phase P1 — The registry
Delivers spec FR-1…FR-15 and FR-26…FR-34: multiple accounts per provider, labels, primary, two-level presets, scheduled health, reconnect.
P1.1 — Contracts and entity
-
T1. Connection contracts. Create
packages/contracts/src/connections/connection.types.tswithConnectionKind,ConnectionBackingKind,ConnectionScopePresetId,ConnectionHealth,ConnectionDto,ConnectionGroupDto,ConnectionProviderDto, and the constantsCONNECTION_LABEL_MAX = 60,CONNECTIONS_PER_PROVIDER_MAX = 10,CONNECTIONS_PER_WORKSPACE_MAX = 100,CONNECTION_ACCESS_ORDER = ['blocked','read','write']. Createpackages/contracts/src/connections/index.ts; modifypackages/contracts/src/index.tsto re-export it. Done when:pnpm --filter @ever-works/contracts buildemits declarations andimport { ConnectionDto } from '@ever-works/contracts'resolves fromapps/api. -
T2.
Connectionentity. Createpackages/agent/src/entities/connection.entity.tsexactly as specified in plan §3.1 — includinglabelNormalized,healthCheckInFlightAt,healthFailureCount,lastErrorCode,lastErrorMessage,lastUsedAt,lastUsedRunId,toolCount, and thetenantId/organizationIdscope columns with no@ManyToOne(entities import cycle — see the note inpackages/agent/src/entities/user.entity.ts). Use@PortableDateColumnfor every date, nevertype: 'timestamp'. Modifypackages/agent/src/entities/index.tsto export it. Test:packages/agent/src/entities/__tests__/connection.entity.spec.ts— asserts the five index names, that every date column is portable, and that both scope columns exist soapps/api/src/scope/scope-stamping.subscriber.tswill stamp it. -
T3. Migration + backfill. Create
apps/api/src/migrations/1791150000000-AddConnectionRegistry.ts.up():CREATE TABLE connectionswith all five indexes, then the three idempotent backfill statements from plan §3.7 (MCP servers, plugin-prefixedaccountrows,repo_connections), each guarded byWHERE NOT EXISTS, ordering bycreatedAtfor the primary choice, suffixing2/3on label collision before the unique index is created, and writingscopePreset='write',health='unknown'.down():DROP TABLE connectionsonly. Generate withcd apps/api && pnpm typeorm migration:generate -d typeorm.config.ts src/migrations/AddConnectionRegistry, then hand-write the backfill into the generated file. Done when: a fresh DB and a DB with existing MCP/OAuth/repo rows both migrate cleanly, the migration is re-runnable with zero additional inserts, and no statement is aDROPor a rename.
P1.2 — Scope presets as a plugin capability
-
T4 (parallel with T3). New plugin capability. Create
packages/plugin/src/contracts/capabilities/connection-scopes.interface.tswithConnectionScopePresetId,ConnectionScopePreset,IConnectionScopesPlugin, andisConnectionScopesPlugin, per plan §7.1. Modifypackages/plugin/src/contracts/capabilities/index.tstoexport * from './connection-scopes.interface.js';(note the.jssuffix — the file uses ESM specifiers). Test:packages/plugin/src/contracts/__tests__/connection-scopes.spec.ts(Vitest) — the type guard is true only whencapabilitiesincludes'connection-scopes'. Done when:pnpm --filter @ever-works/plugin build && pnpm --filter @ever-works/plugin testis green and no existing capability export moved. -
T5. Scope facade. Create
packages/agent/src/facades/connection-scopes.facade.tsexposinggetPresets(providerId),coversTool(providerId, preset, toolName)andproviderScopesFor(providerId, preset);getPresetsreturns[]for a provider that does not declare the capability.coversToolMUST delegate tomatchesAnyToolPatternfrompackages/contracts/src/policy/tool-grant.types.tsso pattern semantics cannot drift from the tool-grant matrix. Modifypackages/agent/src/facades/facades.module.ts(register the provider) andpackages/agent/src/facades/index.ts(export it). Test:packages/agent/src/facades/__tests__/connection-scopes.facade.spec.tswith a mock plugin — declared presets, undeclared →[], pattern identity with the tool-grant matcher. -
T6 (parallel with T5). Declare presets on the git-provider plugin. Modify
packages/plugins/github/src/github.plugin.tsto add'connection-scopes'to itscapabilitiesarray and implementgetConnectionScopePresets()returning thereadandwritepresets with their provider scope strings and tool patterns. Test:packages/plugins/github/src/github.connection-scopes.spec.ts(Vitest) — both preset ids present,read.toolPatternscontains no pattern that matches a mutating tool name, andwrite.providerScopesis a strict superset ofread.providerScopes. Done when:pnpm --filter @ever-works/plugin-github testis green. No provider scope string appears anywhere outside this package.
P1.3 — Registry service
-
T7. Repository. Create
packages/agent/src/database/repositories/connection.repository.tsnext to the existingmcp-server-connection.repository.ts. Methods:listForUser,findById,findByBacking,countForProvider,countForUser,create,update,remove,setPrimaryTransactional(userId, providerId, connectionId)(the two-statement transaction from plan §3.1),claimDueForHealth(limit)(theFOR UPDATE SKIP LOCKEDCAS from plan §6),stampHealth,stampLastUsed. Modify the repositories barrel to export it. Test:packages/agent/src/database/repositories/connection.repository.spec.ts. -
T8. Registry service. Create
packages/agent/src/connections/connection-registry.service.ts,packages/agent/src/connections/connections.module.ts,packages/agent/src/connections/index.ts. Enforces: label 1–60 and case-insensitively unique per(userId, providerId)(label_taken), 10 per provider (provider_limit_reached), 100 per workspace (connection_limit_reached), exactly one primary, primary promotion on delete (oldesthealthy, else oldest), cross-user reads return not-found, and preset changes that need wider provider scopes throwpreset_requires_reapprovalcarrying the re-approval URL. Emits the activity-log entries from plan §9.1 with{ connectionId, label, providerId, field }and never a value. Modifypackages/agent/src/index.ts/ the agent package'spackage.jsonexportsmap to add the./connectionssubpath, matching how./mcpis exported today. Test:packages/agent/src/connections/__tests__/connection-registry.service.spec.tscovering every rule above plus the primary-promotion message. -
T9. Connection-scoped provider ids for extra OAuth accounts. Modify
packages/agent/src/database/repositories/auth-account.repository.ts: addbuildPluginConnectionProviderId(pluginId, connectionId)returning`${PLUGIN_PROVIDER_PREFIX}${pluginId}#${connectionId}`and prepend that form to the candidate list insidefindConnectedProviderAccountwhen aconnectionIdis supplied. Do not touch the@Index(['userId','providerId'], { unique: true })onauth-account.entity.tsand do not add a migration — see plan §2.5. Test: extendpackages/agent/src/database/repositories/auth-account.repository.spec.ts— the connection-scoped id resolves first, the bare plugin id still resolves for legacy rows, and a social-login row is never returned for a plugin lookup.
P1.4 — Health
-
T10. Health classifier (pure). Create
packages/agent/src/connections/connection-health.ts:classifyProbeResult(...)→{ health, errorCode, errorMessage, failureCount }, plus the fixed error-message catalogue. A credential rejection goes straight toexpired; otherwise 1–2 failures →degraded, ≥ 3 →unreachable; success resets tohealthywithfailureCount = 0. The raw provider body is never returned. Test:packages/agent/src/connections/__tests__/connection-health.spec.ts. -
T11. Health service. Create
packages/agent/src/connections/connection-health.service.tswithprobe(connectionId)dispatching bykindto the existing probes (plan §6):OAuthFacadeService.getAuthenticatedUserforoauth,McpConnectionsService.testformcp,PluginValidationServiceforapi_key, the repo credential check forrepo. Timeout 8 s. Writes health via the repository, emitsconnection_health_changedonly on a transition, and raises a notification through the existing notification preferences. Test:packages/agent/src/connections/__tests__/connection-health.service.spec.ts— each kind routes to its probe, timeout is honoured, no transition ⇒ no activity-log row, no probe result ever reaches the log with a token in it. -
T12. Health dispatcher port + service. Create
packages/agent/src/connections/connection-health-dispatcher.ts— a type-only leaf file exportingCONNECTION_HEALTH_DISPATCHER,ConnectionHealthDispatchPayloadandConnectionHealthDispatcher, modelled onpackages/agent/src/tasks-domain/task-dispatcher.ts. Createpackages/agent/src/connections/connection-health-dispatcher.service.tswithdispatchDue(): claim ≤ 200 due rows viaclaimDueForHealth, enqueue one payload each through the@Optional() @Inject(CONNECTION_HEALTH_DISPATCHER)port. Constitution IV: this file must not import@trigger.dev/sdk. Test:packages/agent/src/connections/__tests__/connection-health-dispatcher.service.spec.ts— per-kind due intervals (60/30/360 min), the 200 cap, in-flight claim, and two concurrent ticks producing disjoint sets. -
T13. Scheduled task + probe task. Create
packages/tasks/src/tasks/trigger/connection-health-dispatcher.task.ts—schedules.task({ id: 'connection-health-dispatcher', cron: '*/15 * * * *' }), bootingTriggerInternalModuleexactly likeagent-heartbeat-dispatcher.task.ts. Createpackages/tasks/src/tasks/trigger/connection-health-probe.task.ts— one probe, returning{ ok: false, error }rather than throwing. Modifypackages/tasks/src/tasks/trigger/index.tsto register both. Done when:pnpm --filter @ever-works/tasks buildis green and the dispatcher binding is provided in the API's Trigger adapter module.
P1.5 — API
- T14. Registry controller + module.
Create
apps/api/src/connections/connections.controller.ts,apps/api/src/connections/connections.module.ts, andapps/api/src/connections/dto/connection.dto.ts(UpdateConnectionDto,CreateConnectUrlDto) with class-validator decorators. Endpoints exactly as in plan §4.1, with@ApiTags/@ApiOperation/@ApiResponseand the@Throttletiers listed there (checkis 6/min). Modifyapps/api/src/api.module.tsto importConnectionsModule. Test:apps/api/src/connections/connections.controller.spec.ts, followingapps/api/src/mcp-connections/mcp-connections.controller.spec.ts: auth guard present, cross-user is404not403, throttle decorators present, DTO validation rejects a 61-char label, and no response body contains a token field.
P1.6 — Web
-
T15. API client + server actions. Create
apps/web/src/lib/api/connections.ts(server-onlyserverFetch,X-Scope-Slugattached) mirroringapps/web/src/lib/api/mcp-connections.ts, andapps/web/src/app/actions/connections.tsmirroringapps/web/src/app/actions/mcp-connections.ts. Modifyapps/web/src/lib/constants.tsto add the route constants. -
T16. Registry UI. Create under
apps/web/src/components/settings/connections/:ConnectionsClient.tsx,ConnectionGroup.tsx,ConnectionRow.tsx,ConnectionHealthPill.tsx,ConnectionManageDrawer.tsx,ConnectionPresetChooser.tsx,ConnectionsEmptyState.tsx. Modifyapps/web/src/app/[locale]/(dashboard)/settings/connections/page.tsxto renderConnectionsClientwith four tabs, where the MCP servers tab renders the existingMcpConnectionsClientunchanged (import it, do not fork it). Implement every state in spec §7.1–§7.4: loading skeleton (no spinner, no layout jump), empty, list error, provider-full, attention banner, and the keyboard map (↑/↓/Enter/r/p/c///Esc). The preset chooser is never optimistic (widening may be refused). Test:apps/web/src/components/settings/connections/ConnectionsClient.unit.spec.tsx. -
T17. i18n — P1 keys. Modify
apps/web/messages/en.json: add thedashboard.settings.connectionssub-objectstabs,mcpTab,add,health,preset,banner,limits,errors,empty, plusaccountsOfMax,primary,makePrimary,rename,manage,reconnect,checkNow,disconnect,disconnectHint,lastUsed,neverUsed,seeRuns. Change the value (never the key) ofdashboard.settings.connections.subtitleto"Accounts your agents can use, and exactly what each one may do."and move the old sentence tomcpTab.subtitle. Do not touch any existing…connections.form.*key. Mirror the same key set into all 20 sibling locale files inapps/web/messages/. Every leaf key name is camelCase and contains no literal.. Done when: no user-visible literal remains in any component from T16, andpnpm --filter ever-works-web lintis green. -
T18. E2E — registry, presets, health. Create
apps/web/e2e/flow-connections-registry.spec.ts,apps/web/e2e/flow-connection-presets.spec.ts,apps/web/e2e/flow-connection-health-reconnect.spec.tsper plan §10.3. Done when: all three pass locally and in CI. -
T19. P1 ship gate. Run
pnpm format && pnpm lint && pnpm type-check && pnpm test && pnpm build. Done when: green, the migration applies to a copy of a real database, and the existing MCP settings screen, repository registry and per-plugin settings pages are visibly unchanged.
Phase P2 — Grants and per-call enforcement
Delivers spec FR-16…FR-25 and FR-35…FR-38.
P2.1 — Data model
-
T20. Grant + usage entities and contracts. Create
packages/agent/src/entities/connection-grant.entity.tsandpackages/agent/src/entities/connection-run-usage.entity.tsper plan §3.2–§3.3; modifypackages/agent/src/entities/index.ts. Createpackages/contracts/src/connections/grant.types.tswithConnectionGrantMode,ConnectionGrantDto(requested/effective/clampedBy) and export it from the connections barrel. Note:connection_run_usagedeliberately has no FK toconnections— Runs keep their history when a Connection is deleted (FR-38). Record that in the entity doc-comment. Test:packages/agent/src/entities/__tests__/connection-grant.entity.spec.ts— the unique index has no nullable member (targetIdis the owninguserIdforworkspacerows). -
T21. Migration. Create
apps/api/src/migrations/1791150100000-AddConnectionGrantsAndUsage.ts:CREATE TABLE connection_grants(+ FKconnectionId → connections(id) ON DELETE CASCADE,- its three indexes),
CREATE TABLE connection_run_usage(+ its two indexes),ALTER TABLE plugin_usage_events ADD COLUMN "connectionId" uuid NULL+idx_plugin_usage_connection.down()drops exactly whatup()created. Done when: additive only — noDROP COLUMN, no rename, no type change.
- its three indexes),
P2.2 — The resolution ladder
-
T22. Pure ladder. Create
packages/agent/src/connections/connection-access.ts— side-effect free, mirroring the structure ofpackages/agent/src/policy/tool-grant.ts:resolveConnectionAccess(ceiling, workspaceGrant, agentGrant)→{ effective, requested, clampedBy }computed asminoverblocked < read < write; absence of a row meansinherit; a grant above its ceiling is stored as written and reported as clamped, never rejected (FR-19). Test:packages/agent/src/connections/__tests__/connection-access.spec.ts— the full 3×4×4 truth table, never-widen, clamping, and un-clamping when the ceiling rises. -
T23. Enforcer port + service + cache. Create
packages/agent/src/connections/connection-access.enforcer.ts— a type-only leaf file exportingCONNECTION_ACCESS_ENFORCER,ConnectionAccessResolveInputandConnectionAccessEnforcer { resolve(input), decide(input, toolName) }, modelled exactly onpackages/agent/src/policy/tool-grant.enforcer.ts. Createpackages/agent/src/connections/connection-access.service.tsimplementing it, with the process-local cache (MAX_AGE_MS = 5_000, keyed byuserId, evicted immediately on any grant or registry write in the same process — plan §2.4).decide()maps a tool name to its Connection:mcp__<server>__*→ the MCP Connection whose label is<server>; otherwise the provider's primary (or the Connection the call names) through the scope facade'scoversTool. On lookup failure: return the Connection's own preset, warn-log, neverwrite(FR-24). Modifypackages/agent/src/connections/connections.module.tsto bind the token. Test:packages/agent/src/connections/__tests__/connection-access.service.spec.ts— cache max-age, immediate in-process eviction, tool→connection mapping for MCP and non-MCP names, and the degrade-to-ceiling path. -
T24. Grant service + repository. Create
packages/agent/src/connections/connection-grant.repository.tsandpackages/agent/src/connections/connection-grants.service.ts:getForConnection,setGrant(upsert on the unique index),clearGrant(delete = revert to inherit),listForAgent. Every write evicts the access cache and emitsconnection_grant_set/connection_grant_cleared. Test:packages/agent/src/connections/__tests__/connection-grants.service.spec.ts— one row per(connection, target)under a concurrent double-write,clearGranton a missing row is a no-op success, cross-user is not-found.
P2.3 — Wiring into the run loop
-
T25. Run-assembly filter (FR-23). Modify
packages/agent/src/agents/agent-tool.service.ts: inject@Optional() @Inject(CONNECTION_ACCESS_ENFORCER); insideresolveGrantedTools(line ~661), after the existing tool-grant partition, drop every descriptor whose Connection resolves toblockedor whose name the effective preset does not cover, adding the dropped names to the returnedrefusedarray so the existing WARN logging explains them. Unbound enforcer ⇒ the descriptor list is byte-identical to today's. Test:packages/agent/src/agents/__tests__/agent-tool.connection-gate.spec.ts. -
T26. Per-invocation gate (FR-20/21/22). Modify
packages/agent/src/agents/agent-run.service.ts: ininvokeTool(line 1219), betweenconst descriptor = descriptorByName.get(call.name)andawait descriptor.invoke(...), callawait this.connectionAccess?.decide({ userId, agentId, … }, call.name). A refusal returns{ error: 'Blocked by connection access — <label>' }in the same shape as the existing "not in the allow-list" branch, appends oneWARNagent_run_logsrow with{ toolName, connectionId, connectionLabel, reason }, and the Run continues. The refusal happens before any outbound request is made. Test: extendpackages/agent/src/agents/__tests__/agent-run.service.*.spec.ts— a grant written mid-Run refuses the next call, the Run does not fail, and the log row appears exactly once per refusal. -
T27. Usage buffer and last-used (FR-35/36/37). Create
packages/agent/src/connections/connection-usage-buffer.ts— accumulates(connectionId, runId, agentId, label)and flushes on 10 s, 100 calls, or run teardown; each flush upsertsconnection_run_usageand updatesconnections.lastUsedAt/lastUsedRunId. Modifyagent-run.service.tsto record a hit after a successful invoke and to flush on every run-exit path (the same paths that already callreleaseMcpRun). Modifypackages/agent/src/mcp/mcp-tool-source.tsrecordInvocationto also setconnectionIdon theplugin_usage_eventsrow it already writes (additive; leave the existingworkIdguard exactly as it is). Test:packages/agent/src/connections/__tests__/connection-usage-buffer.spec.ts. -
T28. Usage retention. Modify
apps/api/src/budgets/plugin-usage-cleanup.service.tsto pruneconnection_run_usagerows older than the same 12-month window, inside the same distributed-lock-guarded daily cron. No new cron. Test: extend that service's existing spec.
P2.4 — API and web
-
T29. Grants controller. Create
apps/api/src/connections/connection-grants.controller.tsandapps/api/src/connections/dto/connection-grant.dto.ts(SetConnectionGrantDto,modeis@IsIn(['read','write','blocked'])—inheritis expressed byDELETEonly). Endpoints per plan §4.2, includingGET /api/agents/:agentId/connectionsandGET /api/connections/:id/runs. Modifyapps/api/src/connections/connections.module.ts. Test:apps/api/src/connections/connection-grants.controller.spec.ts— the response always carriesrequested,effectiveandclampedBy; a wideningPUTreturns200with a clampedeffective, never a4xx. -
T30. Manage-drawer agent access + per-agent tab. Create
apps/web/src/components/settings/connections/ConnectionAgentAccessList.tsxandapps/web/src/components/agents/AgentConnectionsClient.tsx. Createapps/web/src/app/[locale]/(dashboard)/agents/[id]/connections/page.tsxfollowing the shape of the existingagents/[id]/mcp-servers/page.tsx(server component,agentsAPI.get+notFound(), nogenerateMetadata). ModifyAgentMcpServersClient.tsxto add a "See all connections for this agent →" link. Do not remove or redirect themcp-serversroute. Render the clamped state, the blocked hint, and the "Changes apply on each agent's next call. Nothing restarts." line from spec §7.3/§7.8. -
T31. i18n — P2 keys. Modify
apps/web/messages/en.json: adddashboard.settings.connections.agentAccess.*and the wholedashboard.agents.connectionsnamespace; mirror into the 20 locale files. camelCase leaves, no literal dot. -
T32. E2E — grants and attribution. Create
apps/web/e2e/flow-connection-agent-grants.spec.tsandapps/web/e2e/flow-connection-last-used-runs.spec.tsper plan §10.3. -
T33. P2 ship gate.
pnpm format && pnpm lint && pnpm type-check && pnpm test && pnpm build. Additionally: run the agent test suite withCONNECTION_ACCESS_ENFORCERdeliberately unbound and confirm zero behavioural diffs, and add a benchmark assertion thatdecide()adds ≤ 2 ms P95 to an invocation.
Phase P3 — Vault and MCP onboarding
Delivers spec FR-39…FR-58.
P3.1 — Vault
-
T34. Vault entity, contracts, migration. Create
packages/agent/src/entities/vault-secret.entity.tsper plan §3.4, usingEncryptedJsonColumnfrompackages/agent/src/entities/_secret-json-column.ts; modifypackages/agent/src/entities/index.ts. Createpackages/contracts/src/connections/vault.types.tswithVaultSecretDtowhosevaluefield is the literal type'●●●●●●●●'(a real secret then fails to type-check). Createapps/api/src/migrations/1791150200000-AddVaultAndMcpInteractiveAuth.ts:CREATE TABLE vault_secrets(+ two indexes) andALTER TABLE mcp_server_connections ADD "authMode" varchar(16) NOT NULL DEFAULT 'header', ADD "oauthTokens" text NULL, ADD "oauthMetadata" text NULL. Modifypackages/agent/src/entities/mcp-server-connection.entity.tsto add the three matching columns (oauthTokensas anEncryptedJsonColumn). -
T35. Vault service. Create
packages/agent/src/vault/vault.service.ts,vault.repository.ts,vault.module.ts,index.ts. EnforcesVAULT_KEY_PATTERN, the 200-entry cap, the 8 KB value cap, replace-and-delete-only semantics,referenceCountmaintenance, andlastUsedAtstamping. There is nogetValuemethod on this service. The only decrypt path is T36. Emitsvault_secret_created/_rotated/_deletedwith the key name, never the value. Refuses writes whenPLUGIN_SECRET_ENCRYPTION_KEYis unset (this path opts out of the dev plaintext-passthrough fallback — plan §9.3). Test:packages/agent/src/vault/__tests__/vault.service.spec.ts. -
T36. Vault-backed credential resolver. Create
packages/agent/src/vault/vault-credential-resolver.tsimplementingCredentialResolverfrompackages/agent/src/policy/credential-resolver.ts: batchresolve(ctx, keys), omits keys it cannot supply (never an empty string), never logs a value, stampslastUsedAt. Modifypackages/agent/src/vault/vault.module.tsto bind it toCREDENTIAL_RESOLVER, taking precedence overEnvCredentialResolverwhen the vault module is imported. Test:packages/agent/src/vault/__tests__/vault-credential-resolver.spec.tspluspackages/agent/src/vault/__tests__/vault-no-read-path.spec.ts— a reflective guard asserting no exported DTO type and no controller method can returnVaultSecret['secret']. -
T37. Vault API. Create
apps/api/src/vault/vault.controller.ts,vault.module.ts,dto/vault-secret.dto.ts(CreateVaultSecretDto,UpdateVaultSecretDto;valueis@IsString() @MaxLength(8192)and@Exclude()on output). Endpoints per plan §4.4. No value-read route, no?reveal, no export, no admin variant. Modifyapps/api/src/api.module.ts. Test:apps/api/src/vault/vault.controller.spec.ts— enumerate every route on the controller and assert none returns a field derived from the encrypted column. -
T38. Vault UI + i18n. Create
apps/web/src/components/settings/vault/VaultClient.tsx,VaultSecretRow.tsx,VaultSecretDialog.tsx;apps/web/src/lib/api/vault.ts;apps/web/src/app/actions/vault.ts. ModifyConnectionsClient.tsxto render it under the?tab=vaultquery param. Modifyapps/web/messages/en.jsonwith thedashboard.settings.vaultnamespace and mirror into the 20 locale files. Implement every state in spec §7.7: grouped list, masked value, add/replace dialog, empty, full, "Not used by any connection". Test:apps/web/src/components/settings/vault/VaultClient.unit.spec.tsx.
P3.2 — MCP onboarding
-
T39. Config parser (pure). Create
packages/agent/src/connections/mcp-config-parser.tsper plan §7.5. ReuseMCP_CONNECTION_NAME_PATTERNexported frommcp-server-connection.entity.ts— do not restate the regex. Test:packages/agent/src/connections/__tests__/mcp-config-parser.spec.tswith fixtures for: trailing comma, markdown fence,//comment, bare"name": { … }fragment,mcpServerswrapper, bare URL, 11 servers, 17 KB input,http://url, invalid name, and a header whose value must be classified as a secret. -
T40. Interactive-auth detection and handshake. Create
packages/agent/src/connections/mcp-auth-detect.ts(probe →header/interactive/unknown) andpackages/agent/src/connections/mcp-authorize.service.ts(metadata discovery, dynamic client registration with a configured-client fallback, authorization-code + PKCE, signed single-usestatebound to(connectionId, userId)with a 10-minute TTL, token storage inmcp_server_connections.oauthTokens, silent refresh on401, refresh failure ⇒ Connectionexpired). Every request in this flow goes throughpackages/agent/src/mcp/guarded-fetch.ts. No server-specific branch may exist in either file. Test:packages/agent/src/connections/__tests__/mcp-auth-detect.spec.tsand.../mcp-authorize.service.spec.ts— state is single-use, an expired state is a400that reveals nothing, a metadata document pointing at a private address is refused. -
T41. MCP onboarding controller. Create
apps/api/src/connections/mcp-onboarding.controller.tsandapps/api/src/connections/dto/mcp-onboarding.dto.ts(ParseMcpConfigDtowith@MaxLength(16384),CreateMcpConnectionFromParseDtowith@ArrayMaxSize(10)). Endpoints per plan §4.3.POST /api/connections/mcpmoves header values into the Vault first and stores only{{cred.key}}references, then delegates row creation to the existingMcpConnectionsService.createso the tenant-inherit binding, name pattern and SSRF guard all still apply.GET /api/connections/mcp/callbackis the only new@Public()endpoint; document the justification in a doc-comment above it. Name-collision check spans installed plugin ids and existing Connection labels; on collision nothing is persisted, including the pasted secret. Depends on T41a: storing a{{cred.key}}reference inauthHeadersis only correct once the MCP client resolves it at connect time; do not merge T41 without it. Test:apps/api/src/connections/mcp-onboarding.controller.spec.ts— parse persists nothing; collision persists nothing; duplicate URL returns the existing Connection id; callback rejects an unknown state. -
T41b. Require HTTPS for any MCP connection that carries credentials. Today
McpConnectionsServicevalidates the endpoint withisSafeWebhookUrl, which accepts publichttp:URLs, and the SDK transport applies headers without upgrading the scheme. Once T41a resolves vault values into headers, a manualhttp://connection would send them in cleartext. Modifypackages/agent/src/mcp/mcp-connections.service.ts: on both create and update, after the existing SSRF check, reject a non-https:URL when the connection'sauthHeaderscontain any{{cred.*}}reference or any non-empty header value. Plain-HTTP connections with no credentials keep working exactly as today (including local development servers), so no existing unauthenticated connection regresses. Also re-check the scheme inMcpClientService.connect()immediately before resolving credentials, so a row written before this rule, or edited out-of-band, still fails closed withCredentials require an https:// endpointrather than sending them. Test: extendpackages/agent/src/mcp/__tests__/mcp-connections.service.spec.ts— create and update both rejecthttp:+ credential reference;http:with no headers is still accepted;https:+ credential reference is accepted — andmcp-client.service.spec.ts— a legacyhttp:row with a credential reference never reaches the factory. Done when: no code path can transmit a resolved vault value over a non-TLS transport, and every existing unauthenticated plain-HTTP connection behaves as before. -
T41a. Resolve MCP header credentials at connect time. Create
packages/agent/src/mcp/mcp-header-credentials.ts— pure, no NestJS import:resolveHeaderCredentials(headers, resolved)built on the existingcollectCredentialRefs/interpolateCredentialsfrompackages/agent/src/policy/credential-interpolation.ts, returning{ headers, missing }as a new object, plusMcpHeaderCredentialMissingError { keys }. Modifypackages/agent/src/mcp/mcp-client.service.tsper plan §4.3.1: inject@Optional() @Inject(CREDENTIAL_RESOLVER); inconnect(), resolve with{ userId, organizationId, tenantId }from the Connection, throw onmissingbeforefactory.connect, and pass the resolved object only to the factory; extendredactHeaderValuesto scrub the resolved values; map the error inclassifyErrortoMissing credential `<key>`. Never assign resolved headers to the entity, the tools cache, a log call or a monitoring breadcrumb. Modify the T10 health classifier (packages/agent/src/connections/connection-health.ts) soMcpHeaderCredentialMissingErrormaps toexpiredwith error codecredential_missing, and extendconnection-health.spec.tsfor it; add the Missing credential copy from spec §7.9 toapps/web/messages/en.json. Test:packages/agent/src/mcp/__tests__/mcp-header-credentials.spec.ts(new) and an extension ofpackages/agent/src/mcp/__tests__/mcp-client.service.spec.ts— resolved headers reach the factory while the entity keeps the reference; a missing key throws before the factory is called and names the key; an unbound resolver fails closed; a header with no reference is sent unchanged; an SDK error echoing the resolved value is redacted; a spy logger and the stampedlastErrornever contain the resolved value. Done when: spec FR-47a and its two acceptance criteria hold, and every existingmcp-client.service.spec.tscase passes unchanged. -
T42. MCP wizard UI + i18n. Create
apps/web/src/components/settings/connections/AddMcpServerDialog.tsxandMcpAuthorizeCard.tsximplementing every state in spec §7.5–§7.6: paste, reading preview, detected-interactive, waiting (with the countdown), connected (tool names), timed out, name collision, duplicate URL, unparseable, address refused. Poll…/authorize/statuson a clientsetInterval(3000), cleared on unmount, terminal state, and at 10 minutes. Modifyapps/web/messages/en.jsonwithdashboard.settings.connections.mcpWizard.*and mirror into the 20 locale files. -
T43. Presets on the connector plugins. Modify each of the 11 packages under
packages/plugins/*-connector/to declare'connection-scopes'and implementgetConnectionScopePresets(). Test: one Vitest spec per package asserting both preset ids andwrite.providerScopes ⊇ read.providerScopes. Done when: no provider scope string exists outside its own plugin package. -
T44. E2E — vault and MCP onboarding. Create
apps/web/e2e/flow-vault-write-only.spec.ts(asserting no network response body in the Playwright trace contains the plaintext),apps/web/e2e/flow-mcp-add-server.spec.tsandapps/web/e2e/flow-mcp-interactive-signin.spec.tsper plan §10.3.
Phase P4 — Documentation and rollout
-
T45. User-facing docs. Create
docs/features/connections-and-scopes.mdcovering the registry, presets, per-agent access, health, the vault and adding an MCP server. Modifydocs/features/index.mdandapps/docs/sidebarsPlatform.tsto list it (the sidebar is manual — an unlisted file renders only as an orphan page). Done when:pnpm --filter ever-works-docs buildreports no broken links. -
T46. Program bookkeeping. Modify
docs/specs/features/agent-workspace/TRACKER.md— mark AW-15 specApprovedand implementationIn Progress/Doneas phases land. Modifydocs/specs/features/agent-workspace/README.md§1 vocabulary table only if a new noun was actually introduced (it is: Connection gains a row, plus Scope preset, Connection grant and Vault credential — add all four in the same PR as P1, P2 and P3 respectively). Modify this epic'sspec.mdandplan.mdstatus fields toImplemented/Done. -
T47. Final gate.
pnpm format && pnpm lint && pnpm type-check && pnpm test && pnpm build, plus the full Playwright suite. Walk the spec §9 acceptance checklist end to end against a running stack and tick every box.
Definition of Done
- Every checkbox above is ticked.
- All three migrations apply forward cleanly on a copy of a real database and are re-runnable with no additional writes.
pnpm format:check,pnpm lint,pnpm type-checkandpnpm testare green.pnpm --filter ever-works-docs buildproduces no broken-link warnings.- The agent suite passes with both new enforcer tokens unbound, with zero behavioural diffs
from
develop(proving the degradation posture in plan §9.3). - A grep of the repository finds no provider scope string outside its own plugin package (Constitution II).
- A grep of the diff finds no log statement, DTO field, activity-log
detailskey or Sentry breadcrumb that can carry a credential value (Constitution VII). - Every constitution gate in
spec.md§11 is confirmed satisfied.