Capability Store (SSOT)
A store — not a private registry — where a workspace's members AUTHOR agent capabilities (managed tool adapters, not raw MCP endpoints) and publish them at one of three reach tiers: private (only me), subset (a chosen subset of your own workspaces — "this skill, in 2 of my 5 workspaces"), or public (every Everdict workspace). One discriminated
Capabilityentity carries three kinds —mcp(a curated MCP connection),code(a python/node tool Everdict executes),skill(instructions) — so a browsing member adopts a capability into their agent instead of hand-typing a server URL. Design confirmed with the user (2026-07-24): ideal structure + flexibility prioritized over least disruption → one unified versioned entity (mirrors theJudgekind idiom), adoption by immutable-version reference (not value copy), skills folded in. Doc-first.
Problem
Today the only agent tool channel is AgentSpec.mcpServers[] (packages/contracts/src/harness/agent-spec.ts): a
member hand-types {name, url, authSecret, write} per server in Settings › Agent. That is:
- Raw, not managed — no adapter that already knows a tool's URL, which secrets it needs, and what it provides; every member re-discovers and re-types the same server.
- Not discoverable — a tool one member wires up is invisible to everyone else; nothing is browsable or reusable.
- Not shareable — there is no way to offer a tool to another workspace, let alone publish one broadly. The only
cross-tenant sharing anywhere in Everdict is the first-party
_sharedregistry fallback (operator-seeded, not user-authored). - MCP-only — a "tool" can only be an external MCP server. A member cannot ship a small python/node function as a tool without standing up and hosting an MCP server.
- Skills are a parallel, weaker channel —
SkillRecord(packages/contracts/src/records/skill.ts) is instructions-only,private|workspace, mutable, and ambient (every visible skill auto-applies viaskillStore.list). It cannot be shared beyond a workspace and is not part of any store.
We want a store: managed, browsable, adoptable capabilities that members author and publish across three reach tiers, spanning tools (MCP + code) and skills under one surface.
Key insight: three layers, one entity
The feature splits cleanly into three layers, each landing on an existing pattern:
① CATALOG (the store) Capability = the SSOT of what exists to adopt (browse · publish · version)
one entity, discriminated: type ∈ { mcp | code | skill }
reach: private | workspace | subset(sharedWith[]) | public
immutable versions (npm-style) + a pure visibility kernel in @everdict/domain
│ browse / publish
▼
② ADOPTION (agent config) AgentSpec.capabilities[] = immutable-version REFERENCES to catalog entries
{ source, id, version, … consumer-side binding } — a pinned, reproducible dependency
upgrade = re-pin to a newer version; the catalog stays the single source of truth
│ resolve (cross-tenant, visibility re-checked, best-effort)
▼
③ RUNTIME (apps/agent) profile.ts resolves each ref → splits by type → type-specific adapter:
• mcp → existing mcpToolToDefinition bridge (runtime unchanged)
• skill → existing use_skill tool (runtime unchanged)
• code → NEW: provision a sandbox ComputeHandle, run the script, parse stdout → ToolResult
Two structural choices (confirmed, chosen for structure + flexibility over least disruption):
- One unified
everdict_capabilitiesversioned table, discriminated bytype— not aTooltable plus a separateSkilltable. This is exactly the idiom Everdict already uses for Judges (model|harness|codeunder one entity). A future capability kind = a newtypevariant + a runtime adapter, with zero new table/store/route/authz-action. Skills (nascent — migration0071) fold in astype:'skill'. - Adoption by immutable-version reference, not value copy — because versions are immutable (a published
x@1.2.0never changes), a{source, id, version}reference is already a reproducible pin. The catalog is the only SSOT;AgentSpecstays thin; the store keeps live provenance ("N workspaces adopted this", "update available", deprecation). A pinned publiccodetool is as audit-safe as a value copy — its version cannot mutate under the adopter — while staying normalized.
The MCP runtime path does not change: an adopted mcp capability resolves to the same bridged tools the raw
mcpServers[] path already produces. The store is a curation/discovery/sharing layer over the existing bridge.
The Capability model
The Zod schema is the SSOT (packages/contracts/src/records/capability.ts); types are z.inferred. The spec is a
discriminated union so each kind validates its own shape.
type CapabilityType = 'mcp' | 'code' | 'skill'
// Reach tier. Extends the `private | workspace` vocabulary (Views / skills / browser-profiles) with the two
// genuinely-new cross-tenant tiers. `workspace = tenant = trust-zone`.
type CapabilityVisibility =
| 'private' // creator-only, within the owning workspace
| 'workspace' // any member of the owning workspace
| 'subset' // the owning workspace + every workspace id in `sharedWith`
| 'public' // every Everdict workspace (cross-tenant read)
// --- the discriminated spec (spec.type is the record's kind) ---
interface McpToolSpec { // a curated, managed MCP connection (the "adapter")
type: 'mcp'
// EXACTLY ONE transport (enforced at the save boundary — SaveCapabilityBodySchema — since a discriminatedUnion
// member can't be a refined ZodEffects). url = a remote server; image = a container Everdict runs over stdio.
url?: string // remote MCP endpoint (Streamable HTTP); auth = requiredSecrets[0] → Authorization
image?: string // container image → `docker run --rm -i <image> [args]` (MCP over stdio); requiredSecrets → --env
args?: string[] // trailing args after the image (stdio only) — e.g. ["-t","stdio"] for grafana/mcp-grafana
provides?: string[] // the tool names this server exposes (for the store card; discovery only)
requiredSecrets: { name: string; description: string }[] // secrets the ADOPTER must supply (declared, never valued)
write: boolean // does this server offer mutating tools (adopter still opts in per-adoption)
}
// Containerized stdio servers are ISOLATED by construction (the container is the sandbox — matching the code-tool
// sandbox discipline and the Docker MCP Catalog distribution) and are OPERATOR-GATED: the agent spawns `docker run`
// only when AGENT_MCP_ALLOW_STDIO is set (default off), and — if AGENT_MCP_STDIO_ALLOWED_IMAGES pins a set — only for
// images on that allowlist; otherwise the capability is skipped (degrade, never fail). Curated
// image-transport servers are seeded in `firstPartyCatalogExtras()` (public + adoptable, NOT default-enabled) —
// e.g. the Grafana MCP server (grafana/mcp-grafana). Self-hosted stdio servers (ClickHouse, Playwright, Qdrant, …)
// are the reason for this transport: they have no universal HTTP endpoint, so `url` alone couldn't publish them.
interface CodeToolSpec { // a python/node function Everdict runs and bridges as a callable tool
type: 'code'
language: 'python' | 'node'
code: string // the source, pinned by version (immutable — auditable)
parametersSchema: Record<string, unknown> // JSON Schema for the tool's arguments (shown to the model verbatim)
isReadOnly: boolean // read-only tools skip the permission gate; writes require consent
timeoutSec?: number
image?: string // optional dedicated sandbox image (else the default hardened sandbox)
requiredSecrets?: { name: string; description: string }[] // env the adopter binds at adoption
examples?: { name?: string; input: Record<string, unknown>; note?: string }[] // worked examples (see below)
}
interface SkillSpec { // the SKILL.md shape (today's SkillRecord), now versioned + shareable
type: 'skill'
instructions: string // the SKILL.md body, loaded on demand via use_skill
files: SkillFile[] // supporting reference files, each loaded individually via read_skill_file
}
type CapabilitySpec = McpToolSpec | CodeToolSpec | SkillSpec
interface CapabilityRecord {
id: string
tenant: string // the OWNER workspace (the publisher)
version: string // immutable; new content = new version (registration-order / semver, like harness/judge)
name: string // the tool/skill name the agent sees (namespaced at runtime)
description: string // the discovery line (store card + the model's when-to-use)
spec: CapabilitySpec
visibility: CapabilityVisibility
sharedWith: string[] // target workspace ids (⊆ the AUTHOR's own memberships); only when visibility === 'subset'
tags: string[]
createdBy: string // subject; owner
createdAt: string
// No updatedAt — versions are immutable (edit = publish a new version). Matches the registry entities.
}
Visibility & sharing (the net-new part)
private/workspace reuse the exact listVisible(tenant, subject) pattern from ViewStore/SkillStore
(visibility='workspace' OR created_by=subject, scoped to the owning tenant). The two new tiers are the first
capabilities to be readable from a workspace other than the one they live in — but they are two very different acts:
subset fans a capability across the author's own workspaces, public exposes it to everyone.
- subset — the author shares to a chosen subset of the workspaces they themselves are a member of: "this
skill, in 2 of my 5 workspaces." A multi-select over the author's own memberships →
sharedWith[](validated⊆ membershipsat publish). A workspaceTreads it iffvisibility='subset' AND T = ANY(sharedWith)(the owner always reads); every member of a target workspace then sees it there. This is not publishing to strangers' workspaces — that ispublic. Because the targets are the author's own trust zones, no org/group tenancy layer and no accept/invite handshake is needed; the author fans out unilaterally and can revoke by dropping a workspace fromsharedWith. Member-gated (a member owns fanning out their own capability). - public — the real "expose to everyone" tier: readable by any authenticated subject in any Everdict
workspace (a dedicated
listPublic()read path, no tenant filter). This is where the genuine trust-boundary cost lives, so settingvisibility='public'is admin-gated by default (publishing globally is a heavy act). Instance policy (EVERDICT_ALLOW_MEMBER_PUBLIC_PUBLISH) relaxes this: a self-hosted operator running a community instance sets it so any member — not only an admin — may publish/promote topublic. It is a deployment property ("is this a shared-catalog instance?"), not per-workspace state, so it lives in operator config (no migration), is injected intoCapabilityServiceasallowMemberPublicPublish, and is surfaced to the web onGET /me → config.allowMemberPublicPublishfor UX gating (the service still enforces). ThemayPublishPublic(actor)helper is the single authority — bothsave()(new-capability create) andsetVisibility()(reach promotion) consult it.
A pure kernel in @everdict/domain — canConsume(capability, { tenant, subject }) and
visibleCapabilities(all, { tenant, subject }) — is the single authority, reused by the store service (browse) AND
the runtime resolver (adoption). Writes (edit-as-new-version / delete / change visibility) are creator-or-admin,
owner-tenant only — the same gate as ViewService, injected as actor={subject,isAdmin}.
Adoption (reference, pinned, cross-tenant)
AgentSpec gains a capabilities[] field of pinned references; the existing mcpServers[] stays as the raw
escape hatch (power users, or a server not worth publishing — mirrors openai-compatible as the LLM escape hatch).
interface CapabilityRef {
source: string // the owner workspace that published the capability (= my tenant for private/workspace)
id: string
version: string // the pinned immutable version (reproducible)
// consumer-side binding, layered on the reference at adoption:
secretBindings?: Record<string, string> // required-secret name → one of MY workspace's secret names
enableWrite?: boolean // opt in to a write-capable mcp/code capability (default false)
}
// AgentSpecSchema gains: capabilities: z.array(CapabilityRefSchema).default([])
Runtime resolution (apps/agent/src/profile.ts, per turn, best-effort like today's secret/skill resolution):
for each ref, capabilityRegistry.getForConsumer(source, id, version, { tenant, subject }) loads the pinned record
and re-checks canConsume (access may have been revoked / unpublished → skip that capability, degrade, never
fail the turn). Resolved records are split by spec.type and handed to the type adapters below. Because the version
is immutable, an eval run that uses this agent is reproducible; the store surfaces "update available" by comparing a
ref's pinned version to the latest visible version.
Skills become explicitly adopted, not ambient. Once a capability can be public, auto-applying every visible
skill is absurd (you would inherit every public skill on Everdict). So an agent uses only the skill capabilities it
has adopted — a deliberate behavior change from today's skillStore.list ambient model, and the correct one for a
store.
Version management (parity with the registry entities)
All four kinds (mcp | code | skill | environment) are versioned on ONE substrate, so versioning is uniform by
construction: (tenant, id, version) is immutable, a content edit auto patch-bumps (latest moves; pinned adoptions
stay reproducible), and per-version tags are mutable metadata OUTSIDE spec immutability. The full management surface
mirrors the registry entities (harness/dataset/judge/runtime) — one service core (CapabilityService), two transports
(BFF + MCP), one web drill-in:
- List versions —
GET /capabilities/:id/versions+list_capability_versions→ the ascending live versions plus aversion → tagsdisplay map.?source=reads a cross-tenant public/subset owner (so the store can show the history of a capability published from another workspace). - Version tags —
PUT /capabilities/:id/versions/:version/tags+set_capability_version_tags→ replace a version's free-form labels (trimmed / deduped, ≤20×60, reusingnormalizeVersionTags). Gate:capabilities:writePLUS the version's creator-or-admin (thedeleteVersiongate); own-workspace versions only. - Version diff —
GET /capabilities/:id/diff?base=&candidate=+diff_capability_versions→ a structural diff over the immutable content (name/description/spec) via the shareddiffSpecFieldsengine (the same one behind the harness/judge diffs);typeChangedflags an mcp ↔ code ↔ skill ↔ environment restructure.?source=diffs a cross-tenant public/subset owner. Reproducible by the immutable-version guarantee (CapabilitySpecDiff). - Reads —
GET /capabilities/:idandGET /capabilities/:id/versions/:versionalso take an optional?source=, so the store's version switcher can inspect an older version of a public capability owned by another workspace. - Web — the store detail drill-in (
CapabilityVersionsPanel) adds a version switcher (loads any version's spec), the sharedVersionTagsEditor(entity="capability", editable only for an own-workspace creator/admin), and an inline base ↔ candidate diff. Built-ins (_everdict) are code-defined single-version → no panel.
Runtime consumption (per-type adapters)
apps/agent/src/mcp-tools.ts builds the ToolRegistry; each resolved capability becomes one or more
ToolDefinitions:
mcp— two transports resolved inprofile.tsto aResolvedMcpServerunion. http (url): eachsecretBindingsvalue → workspace SecretStore value →Authorizationheader; connect via Streamable HTTP. stdio (image): eachrequiredSecrets→ the adopter's bound secret value → a container env var; the agent connects viaStdioClientTransportrunningdocker run --rm -i --env NAME … <image> [args](secret VALUES ride in the spawned process's env, only--env NAMEon argv — nops/log leak). Both bridge withmcpToolToDefinition, namespacedmcp__<name>__<tool>, write-filtered byenableWrite. stdio is skipped unlessAGENT_MCP_ALLOW_STDIOis set, and skipped when a required secret is unbound. Private images: the docker CLI inherits the operator's host credentials (the agent forwards onlyHOME/PATH— not its own secrets — to the docker process), so a private image pulls via the host'sdocker login/ credential helpers. Per-workspace registry credentials (the workspace image-registry pull auth) into the docker pull is a future item — the operator-host login covers the managed case.skill— feed{name, description, instructions, files}into the existingbuildSkillTools→ theuse_skill(+read_skill_filewhen files exist) tools. Zero new runtime code.
Code-tool verification — nobody adopts by reading source
A code capability carries worked examples ({name?, input, note?} — concrete argument objects), and they do
triple duty: the store detail shows them (what the tool DOES, not just its code), the try-runner executes them, and
the agent bridge appends up to two to the bridged tool's description (the model learns the call shape from a real
invocation, alongside parametersSchema). Verification runs on the agent service (POST /agent/code-tools/try,
mirroring the skill test-drive):
-
check — parse-only compile validation (
node --check/python3 -m py_compile): the source is written into a fresh handle and parsed, never executed — safe for any target. The wizard offers it before publish. -
run — execute the tool against an example input under the agent's EXACT execution contract (input JSON as argv[1], last-JSON-on-stdout result, per-call handle, dispose in finally) and the same sandbox gate: a target from ANOTHER workspace runs only on an isolated runtime, refused otherwise. Targets are an unsaved draft
spec(the wizard) or a published{source,id,version}ref (the store's try panel) — the ref is resolved and visibility-re-checked server-side, so the client never asserts trust.requiredSecretsbind by their declared name from the caller's workspace → personal secrets; unresolved names come back inmissingSecretsso a failing run is explainable rather than mysterious. -
code— NEW. Register aToolDefinition(namefrom the capability,parametersJsonSchema= the spec'sparametersSchema,isReadOnlyfrom the spec) whosecall(input, ctx):- provisions a sandbox
ComputeHandle(see security), - writes the tool
input(validated againstparametersSchema) as a JSON context file, - runs the pinned source (
python3 <script> <context-path>— the exact script-grader contract frompackages/graders/src/script-grader.ts: context JSON in,ToolResult-shaped JSON on stdout), - parses stdout →
ToolResult, disposes the handle infinally.
This reuses the mature
Driver/ComputeHandle/script-execution contract wholesale — the "new execution path" is a thin adapter, not new infrastructure. - provisions a sandbox
Security: code capabilities
A public/subset code capability means running another workspace's code. Non-negotiable:
- Sandbox mandatory for adopted-from-others code — a
codecapability whosesource !== tenantruns in a hardenedDockerDrivercontainer (no host FS, network gated by policy), neverLocalDriveron the control-plane host. Your-own-workspace code in dev may useLocalDriver. - Explicit consent at adoption — adopting a
codecapability surfaces its (immutable, inspectable) source and requires an explicit confirm; the pinned version cannot change under you afterward. isReadOnlyhonored — a write-capable code tool goes through the same permission gate as write MCP tools.publicpublish is admin-gated and a candidate for later operator review.
Authz
New resource actions on the domain matrix (packages/domain/src/auth/authz.ts), replacing the nascent skills:*:
capabilities:read— viewer+ (browse the store, resolve adopted refs).capabilities:write— member+ (author / publish a new version / adopt into one's agent). Settingvisibility='public'additionally requires admin (service-enforced via the injectedactor, no separate action — avoids knob proliferation, mirrors the View gate).capabilities:delete— creator-or-admin (soft-delete a version / tombstone the capability).
Architecture & slices
Follows the established entity pattern (service core + two transports [HTTP + MCP] + mem/Pg stores + Zod at every
boundary + a pure-HTTP web mirror), like views/schedules. Each phase ends on a green gate.
Phase 1 — the Capability entity + visibility kernel + storage
@everdict/contracts:capability.ts(CapabilitySpecdiscriminated union,CapabilityRecord,CapabilityVisibility,CapabilityRef); extendAgentSpecSchemawithcapabilities[].@everdict/domain: the pure visibility kernel (canConsume/visibleCapabilities) + the three new authz actions.@everdict/db:everdict_capabilitiesmigration — the versioned shape(tenant, id, version, spec jsonb, created_at, created_by, deleted_at)plus indexedtype,visibility,shared_with jsonb,tags jsonbcolumns for the browse/visibility queries (a specialized versioned store, likeViewStoreextends the base shape). Data migration foldseverdict_skills→type:'skill'rows (version 1.0.0);0071becomes a no-op/dropped.packages/registry(ordb):CapabilityStore— InMemory + Pg —register(immutable/soft-delete/revive),getForConsumer(visibility-checked, cross-tenant),listVisible(tenant, subject),listPublic,versions,softDelete. Unit-tested against both impls.
Phase 2 — control-plane API + MCP parity
apps/api:CapabilityService(CRUD, publish-version, visibility change with the admin gate forpublic, adopt helpers) + routesPOST/GET /capabilities,GET /capabilities/:id/versions/:v,PATCH(visibility/tags),DELETE+ BFF↔MCP tools (list/get/create/delete_capability,set_capability_visibility). Gated on the new actions. Cross-tenantlistPublic/subset reads honored.
Phase 3 — adoption wiring
AgentSpec.capabilities[]end-to-end:agent-servicesave path,apps/agent/src/profile.tsresolves refs cross-tenant +canConsumere-check + best-effort degrade; rawmcpServers[]retained as the escape hatch.
Phase 4 — runtime adapters
- 4a —
mcp+skillcapabilities load through the existing bridge /use_skill(reuse). - 4b — the
codeadapter: sandboxComputeHandleprovision + script-contract exec +ToolResultparse (apps/agent+ a small shared exec helper reusing the driver/script-grader machinery).
Phase 5 — the web store surface
apps/web:/{workspace}/store— browse (union of visible capabilities; filter by type mcp/code/skill; search; tags; reach badge), detail (description · provides · required secrets · versions · author · Adopt), author flow (type picker → type-specific form: MCP url+required-secrets, code editor + params schema, skill instructions; visibility picker + a workspace-picker forsubset). Settings › Agent lists adopted capabilities + secret bindings; Settings › Skills migrates into the store. FSD slices, next-intl catalogs,settings-list.
Phase 6 — public hardening
- Enforce the sandbox for adopted-from-others
code; adopt-time consent forcode; thepublicadmin gate; (later) operator review, ratings/usage, deprecation propagation.
Web IA — management in Settings, the store as discovery (confirmed 2026-07-28)
The one store page originally mixed three different owners: browse/adopt (discovery), the workspace's own
publications (management), and the imported-environment inventory (settings:write-shaped workspace state). Confirmed
split — no API/authz change, web IA only:
/store= discovery only. Browse + adopt/import over the public catalog (public + first-party managed capabilities); rows show an in-workspace badge instead of management actions. The workspace's own publications live on/store/mine(all kinds in one list) and in the kind-scoped Settings pages below — both render the sameCapabilityStorevariant='mine'.- Settings › Agent group = the agent as one concern:
/settings/agent(config + adopted refs + default-tool toggles) ·/settings/tools(the member's own toolset — see below) ·/settings/skills(living workspace skills) ·/settings/knowledge(the knowledge graph, moved from the Workspace group). - Settings › Workspace › Environments (
/settings/environments) — environments are eval infra, not agent config, and (unlike tools) get a dedicated environment-first surface, not the reused store chrome (EnvironmentWorkbench+EnvironmentEditor, 2026-07-29): one unified list merges the workspace's authoredenvironmentcapabilities with the imported-environment inventory per identity (source/id) — rows speak environment vocabulary (benchmark chip · visibility/pull badges · in-place expand rendering the agent-contract markdown + preset), with authored-row manage menu (edit / reach / delete), inventory re-check / remove, and an inlineauth-failure escape to Settings › Integrations (registry + pull secret). Authoring is an environment-only dialog sectioned by journey (basics → image [+ registry tag helper] → contents → agent contract [scaffold template prompting entry points / result paths + markdown preview] → wiring preset [advanced, collapsed, live JSON validation] → reach), and new environments default toworkspacevisibility (team sharing is the surface's purpose; the store wizard'sprivatedefault stays for other kinds). That default is the SERVICE's, not the form's (E6):CapabilityService.savepicks the first version's reach by kind when the caller omitsvisibility—environment→workspace, tool kinds →private— so the API/MCP path (an agent registering the image a member just pushed) can't quietly create a team asset nobody but its author can see. A tool kind is one member's agent's until shared; an environment is what a harness pins, held workspace-wide onWorkspaceSettings.adoptedEnvironments. Discovery/import of other workspaces' environments stays in/store(linked). The store substrate (entity, versions, reach kernel, routes) is unchanged — presentation only.
The store DETAIL is a route, not a dialog (confirmed 2026-07-31)
A store row is a LINK to /{workspace}/store/{source}/{id} (?from=mine when the entry point was the workspace's own
publications — it picks the back link and shows the reach badge). The detail used to be a modal over the list; it is
now a page, for the same reason the tool detail is (### The tool DETAIL): the right-hand infra/chat panel is half the
workflow, a full-screen spec (code + try-runner, SKILL.md + attachment tabs, an environment's agent contract) does not
belong in a box over the list, and a published capability deserves an address a member can share.
- The page fetches the record server-side (
getCapability(id, source)— first-party_sharedentries resolve there too) and 404s on anything the caller cannot see, so a foreign private publication stays indistinguishable from a missing one.?version=is not part of the address: the version switcher stays an on-demand client read (CapabilityVersionsPanel), because inspecting an old version is a lens on the same entity, not another entity. - The detail is the only surface that adds/removes: list rows stay read-only (manage menu aside), and
CapabilityDetailViewowns the whole per-kind decision — agent adoption (agents:write, with the secret-binding / write-opt-in dialog), skill copy-into-library (skills:write), environment import + pull re-check (settings:write). Mutationsrouter.refresh()the page so its own "in your workspace" state is re-read from the control plane, on top of the server actions'revalidatePathof the list surfaces (which now includes this dynamic route,'page'typed).
The member's agent: Tools + Skills are per member (confirmed 2026-07-29)
A workspace is not one agent. Two members of the same workspace want different tools on the assistant they talk to and
different procedures it follows, and before this the only knobs were workspace-wide: AgentSpec.capabilities[] (so
adopting a tool handed it to everyone) and the skill library (so every member's agent carried every skill). Confirmed
direction (2026-07-29, with the user): the workspace is the shared BASELINE — "which tools and skills this workspace
supports" — and each member overlays their own on/off on top of it.
- Both pages are a list and a switch.
/settings/toolsshows every tool the caller can put on their agent;/settings/skillskeeps its authoring surface (create · edit · share · delete) and gains a per-row "use" switch. Rows group by scope —personal(published/drafted private by this member, visible to them alone) ·workspace(adopted on the AgentSpec, hand-wiredmcpServers[], authored here, or published workspace-wide) ·builtin(the first-party defaults). On the Tools page publishing, versioning, reach, the catalog and the workspace counts are NOT present (user decision) — authoring and discovery stay in/storeand/store/mine. Settings › Account no longer carries a "My tools & skills" tab: a member's private tools and private skill drafts appear in thepersonalsection of the two Agent pages. - The overlay.
AgentMemberPreferences((tenant, subject) → {tools, skills}, eachkey → boolean, mig 0090) is per member and self-scoped, like a personal secret. An ABSENT key means "follow the workspace" — clearing an override deletes the key rather than freezing today's baseline value, so a later workspace change still reaches that member. Keys namespace the channels:default:<id>·capability:<owner>/<id>·mcp:<name>(tools) ·skill:<id>(an authored Skill record). - The baselines. ON for everyone: adopted capabilities, hand-wired MCP servers, the workspace's authored skills (plus the caller's own drafts), and non-opted-out first-party defaults. Listed but OFF until the member switches them on: a capability published here that nobody adopted — tool or skill kind — and the member's own private publications. Existing workspaces therefore behave exactly as before until someone touches a switch.
- One decision point.
resolveAgentCapabilities(@everdict/application-control) assembles both candidate pools in ONE pass over the baseline (adopted references carry both kinds; visibility re-checked cross-tenant), overlays the member's preferences and resolves name shadowing through the pureselectForMemberkernel (@everdict/domain) — which is what keeps "an authored skill shadows a same-named package" and "an adopted tool shadows a built-in" true per member rather than per workspace. BOTH the settings pages (GET/PUT /agent/tools+/agent/skills, with thelist_agent_tools/set_agent_tool/list_agent_skills/set_agent_skillMCP twins) and the agent runtime (apps/agentprofile resolver, per turn, keyed byprincipal.subject) read it — so what a member configures and what their agent carries cannot disagree. Skill FRESHNESS coverage stays an authored-record concern, computed for the enabled authored skills in one batched pass. - Shadowing is per channel: a tool and a skill may share a name (the model reaches them through different doors —
a tool call vs
use_skill). The workspace-wide default-tool toggles on Settings › Agent remain the admin baseline. - A third channel rides the same overlay: the MODEL the member's agent thinks with (
AgentMemberPreferences.model, mig 0167 —GET/PUT /agent/model, Account › Preferences). Not a capability decision, so it is not part ofresolveAgentCapabilities; it is read by the same profile resolver and follows the same reset rule (null= follow the workspace'sAgentSpec.model, never a frozen copy of it). Resolution order + the crafted-agent and verifier exceptions:docs/models.md§"Which model a CONVERSATION runs on".
The tool DETAIL (confirmed 2026-07-29)
The list is a switch; /settings/tools/<urlencoded key> is the explanation behind the switch — a ROUTED page (never a
dialog: the right-hand chat panel is half the workflow). GET /agent/tools/:key (get_agent_tool) returns the row
plus what the tool actually IS, all derived from the same resolveAgentCapabilities pass the runtime uses:
transport— the three things the agent really does:http(open an MCP session) ·stdio(docker run -ia container) ·code(write the source into a sandbox and run it). Rendered as one sentence + the real target.functions— what the tool puts in front of the model, under the NAMESPACED name the model calls. Acodecapability is exactly one function; anmcpserver contributes as many as it serves. The bridged name has ONE spelling —mcpBridgedName/codeBridgedNamein@everdict/domain— used by the runtime bridge AND by this page, so what a member reads is what gets registered. The DECLARED list is the author'sprovides;POST /agent/tools/:key/probe(probe_agent_tool) connects AS THIS MEMBER with their bound secret and replaces it with the server's own answer. Probing is HTTP-MCP only (a stdio container is the agent's to start, a code tool is verified by RUNNING it — the store's try-runner, reused here; a first-party default resolves from the shipped definitions since it has no store row, and stays trusted on a host runtime).secrets— each declared name with the secret name it actually reads and whether the member can satisfy it. Every channel's binding lives on the AgentSpec — an ADOPTED capability on itsCapabilityRef.secretBindings, a hand-wired server on itsauthSecret, and a first-party default / published-but-unadopted capability on the spec-leveltoolSecretBindingsoverlay (tool key → declared name → workspace secret name; without an entry they bind by the declared name) — soPUT /agent/tools/:key/secrets(bind_agent_tool_secrets,agents:write) rewrites any of them and cuts a new agent version (bootstrapping the chat config when a fresh workspace has none). The page therefore offers the same secret picker everywhere: select one of your existing secret names or create one inline; a member withoutagents:writestill gets "store a secret under exactly the bound name". Names only, never values.- Editing is the chat, not a form.
editable(a capability THIS workspace owns) surfaces "대화로 편집하기" → thetoolreference type (get_capability, carryingsourcesince a tool may be owned elsewhere) + thetoolEditmission; the agent reads the spec and publishes a new version under HITL approval. Built-ins and other workspaces' publications are read-only here.
First-party default toolset (confirmed 2026-07-27)
The store as designed above is adopt-only: a capability reaches an agent solely via an explicit
AgentSpec.capabilities[] pin. But an agent should ship with tools out of the box — web search, PDF reading, and
the "use the integration" actions for whatever integrations a workspace has configured — without any member browsing
the store first. And those same tools must stay marketplace-installable (a workspace can swap in a richer/custom
version). Confirmed direction (2026-07-27, with the user): deliver both channels on one substrate — the
Capability entity — by adding a first-party, default-enabled tier. No parallel "built-in tools" list in code; a
default IS a capability, so it is browsable, versioned, and replaceable like any other.
The tier
- First-party = operator/Everdict-authored capabilities owned by a reserved
_everdicttenant (mirrors the_sharedregistry fallback), readable by every workspace. - Browsable in the store — the built-ins are code-defined (
firstPartyDefaults(), not DB rows), soCapabilityService.listPublic()merges them ahead of the DBpubliccatalog (firstPartyCataloginjected). This is what makes "the same tool, two channels" true in the store surface, not just in the agent runtime: the public tab shows the built-ins with a "built-in" badge (owner_everdict), and because they aren't DB rows they are read-only there (no edit/reach/delete — even for an admin) and shown as provided by default rather than an Adopt button (they're managed via Settings › AgentdisabledDefaults, not adoption). - Tools only. The default tier is
web_search/fetch_url/pdf_read— capabilities of the product. Everdict's SKILLS are store examples a workspace copies (see Phase 10 below), never defaults. - Default-enabled = the agent includes them without an
AgentSpec.capabilities[]pin. The effective toolset:first-party default capabilities (auto, gated) ← web search · PDF · integration use-actions∪ adopted capabilities (explicit pins) ← richer / community / custom∪ raw mcpServers[] (escape hatch) - Gated — an integration default is on only when its integration is configured (Mattermost set → the Mattermost tools appear; GitHub App installed → the GitHub tools; a registry set → the image tools). A generic default (PDF) is unconditional; web search is on when a search-provider key is resolvable.
- Opt-out & shadow —
AgentSpec.disabledDefaults[](capability ids) turns a default off; adopting a capability with the samenameshadows the default (the pinned, adopted version wins). Defaults never silently override a member's explicit choice.
Secret resolution for first-party defaults
A default declares requiredSecrets like any capability, but its values resolve from the workspace's existing
integration config, not a manual secretBindings map at adoption (there is no adoption step):
- Mattermost tools → the configured bot token (
workspace/mattermost). - GitHub tools → the workspace GitHub App installation token (already minted for clone/CI).
- image-registry tools → the registry push/pull credentials.
- Web search → a search-provider key: an operator-global key (Everdict runs search for every workspace) or, absent that, a workspace-bound secret; unresolved → the tool is listed as "configure to enable," never a hard failure.
Slices (additive to Phases 1–6)
- Phase 7 — first-party tier mechanism. Reserved
_everdictowner + adefaultEnabled/requires(mattermost | github | image-registry | null) marking on the record;CapabilityStore.listDefaults(); a pure domain gateapplicableDefaults(defaults, { integrationsConfigured });profile.tsmerges resolved defaults (secrets from integration config) with adopted caps and honorsdisabledDefaults[]+ name-shadowing; web surfaces defaults in Settings › Agent (per-default toggle) and flags them "built-in" in the store. - Phase 8 — seed the generic tools. A PDF
codecapability (python, extract text from a URL/artifact; no secret; default-on) and a web-searchcodecapability (portable search API — Tavily/Brave/Serper — behind a search-provider key; default-on when resolvable). Portable API over any provider-native web_search so it works across Anthropic + OpenAI harnesses. - Phase 9 — rich integration adapters. First-party
code(or hostedmcp) capabilities beyond the current three use-actions: Mattermost (list channels · read/post thread), GitHub (create issue · comment on PR/issue · read repo file · list PRs/issues), image-registry (list images/tags · inspect) — each gated on its integration, secrets auto-bound from config, writes HITL-gated. - Phase 10 — first-party SKILLs (landed), then REFRAMED as store examples (2026-07-29, user decision). Everdict
authors two skills —
scorecard-fix-pr(the eval→fix loop as a procedure: diagnose a scorecard's failing cases from the eval evidence, locate the root cause viaget_github_file, open the fix PR viaopen_github_prwith the experiment context MANDATORY in the body) andtrace-analysis— but they are NOT a default tier. They are EXAMPLES that live in the store (firstPartySkillExamples(), merged into the public catalog) until a workspace takes one.- A skill is a document a workspace owns. Shipping one as a silent default puts a procedure in every
workspace's agent that nobody there wrote, can edit, or can version — which is exactly what the reframe removes.
Tools stay defaults (nobody edits
web_search); skills do not. - Taking one COPIES it (
POST /skills/import→SkillService.importFromStore): the content lands as an ordinary workspaceSkillRecord,visibility: workspace, its version line starting at the version copied, withorigin: {source, id, version, name}as provenance (never a live link — the moment they edit it, a link would either fight their edits or lie). The store hides an example a workspace already took; taking it twice is 409. - Skill-kind capabilities are not agent attachments at all.
resolveAgentCapabilitiesresolves skills from ONE channel — the workspace's ownSkillRecords — so Settings › Agent › Skills lists exactly what the agent follows, and every entry is editable and versionable by the people it belongs to. Publishing a skill to the store hands others something to copy; it does not add a second, uneditable row to anyone's library (not even the author's). The oldorigin: authored | packagedsplit onGET /agent/skillsis gone with it. scorecard-fix-pr'srequires: "github"gate went with the default tier: nothing is auto-attached, so there is nothing to gate. The copy tells the agent to use the GitHub tools, and those are gated on their own. See "Skill versions" below.
- A skill is a document a workspace owns. Shipping one as a silent default puts a procedure in every
workspace's agent that nobody there wrote, can edit, or can version — which is exactly what the reframe removes.
Tools stay defaults (nobody edits
The base control-plane surface is now bridge-all (docs/architecture/agent-conversations.md P13): every entity's
reads AND mutations reach the agent (only the runner wire-protocol tools are excluded), with each mutation decided by
the session's permission mode (default=ask · auto=ask only guarded actions · bypass · plan) on top of the RBAC. The
former curated INTEGRATION_ACTIONS admission list is gone — the integration "use" actions (post_mattermost_message,
open_ci_setup_pr, open_github_pr, …) are simply part of that surface, and they still migrate into first-party
integration capabilities as the richer Phase 9 adapters land.
Fourth kind — environment (managed eval-environment images)
The substrate's extensibility claim has been exercised: type:'environment' publishes a managed eval-environment
image (pullable ref + composition preset + instructions) into the same store — consumed at harness-AUTHORING time
(template pins / service images), not adopted as an agent tool. Full design + slices:
docs/architecture/environment-image-store.md.
Fifth kind — delegation (a work environment everdict hands work TO)
The other four kinds describe what an agent USES. type:'delegation' describes an environment everdict
employs: a registered work-agent it can hand a job to and converse with until the job is done.
Why a capability and not a harness. A harness is the agent under test — the eval lane's subject. A
delegate is the opposite role: it is the worker. What a workspace needs of it is exactly what the store
already provides — versioning, the four reach tiers, cross-tenant sharing, adopt-and-edit, and a first-party
EXAMPLE to start from. (The harness registry could not carry it anyway: ProcessHarnessSpec is
{kind,id,version}, makeHarness discarded everything else, and model resolution skipped process kind
entirely — so a "claude-code with THIS image, model and env" had no representation at all.)
What it pins — one reference collapses what a delegation otherwise re-specifies per call:
harness (which conversational agent runs — any adapter carrying the conversational marker) · image
(prebuilt, so a delegation costs no per-session install) · model (a registered Model → baseUrl + underlying
model + key, ModelRef.env remapping included) · env (literal or {secretRef, scope}) · workDir (the
conversation's stable cwd) · instructions + instructionsFile (the STANDING brief, seeded as the file that
agent reads by convention — CLAUDE.md · AGENTS.md · …) · ttlSec.
Env precedence, stated once: harnessAuthEnv (workspace→personal tiers) < the profile's own env < the
model's connection env. Same "model wins" rule the eval lane applies, so a profile and a harness never
disagree about who owns the endpoint.
The handoff is a contract, not a prose blob. POST /sandboxes {profile, brief} — the brief (goal ·
context · references · constraints · done-criteria; the reference TYPE vocabulary is the agent's own) is
rendered once (renderDelegationBrief, @everdict/domain), written into the delegate's working directory as
BRIEF.md before the ledger row exists (a delegate that silently never got its context is the failure
this ordering prevents), and sealed on the session trajectory as a delegation.brief marker — so the ledger
alone answers what they were actually asked to do. A profile session is always a conversation; turns run in
the profile's own workDir, never a per-task scope, or the delegate walks away from its brief.
WHO is a separate axis from WHERE. A profile is an OVERLAY on the session's target, not a boot mode — a
delegate must be able to work anywhere a member can: alone it runs in its own image; with world it continues
that world (and its work hibernates into the next snapshot) or FOUNDS one, taking the profile's image as the
genesis base when the caller names none; with environment/image it works in that one; repo clones in as
usual. The only conflict is harness, which also says who runs.
Refused by name: a brief without a profile, profile + harness, a profile whose harness cannot converse,
and a profile naming a secret the workspace has not set.
Scope note (live-verified): the profile's env is the DELEGATE's environment — it reaches the agent
adapter, not the session's exec channel (which is the operator's own shell, and has never carried
apiKeyEnv either). If a delegation needs a variable present for hand-run commands too, bake it into the
image; making exec inherit the agent's environment would quietly hand an operator shell the delegate's
credentials.
Non-goals (this iteration)
- No org/group tenancy layer —
subsetis an explicitsharedWith[]. - No accept/invite handshake for
subset— the owner shares unilaterally (revocable). - No live-reference adoption (auto-updating) — refs are pinned; upgrade is an explicit re-pin.
- No marketplace economy (payments/ratings/reviews) in v1 — provenance + "update available" only.
- No value-copy adoption — the catalog is the SSOT.
Open questions
- Secret-binding UX for
mcp/codeat adoption — map each declaredrequiredSecrets[].nameto a workspace secret via the existingSecretPicker; unbound required secret → block adoption or warn? publicmoderation — admin-gate is v1; do we need operator review / a report flow before a global marketplace?codesandbox network policy — default deny-all egress, or an allowlist the author declares and the adopter approves?- Namespacing collisions across many adopted capabilities —
mcp__<name>__<tool>/code__<name>; enforce uniquenameper agent at adoption. - Skill migration — confirm
everdict_skillshas no production data worth preserving beyond the fold-in.
Skill versions (2026-07-29)
A workspace skill carries its own semver, so "edit it in conversation, then stamp the version" is a real loop:
- The row is the WORKING COPY. Members (and the agent, via
update_skillunder the session's permission mode) edit it freely;SkillRecord.versionnames the last content the workspace decided to publish, not every keystroke. - A stamp freezes content (
POST /skills/:id/versions/ MCPstamp_skill_version):bump(major|minor|patch, default patch) or an explicit version that must order above the current one (else 400), plus an optionalnote(the changelog line). The snapshot is immutable — a re-stamp of a live version is 409 — which is what makes "what did this procedure say when we ran that eval?" answerable. Content is read filesystem-first, so a body an agent rewrote through the workspace filesystem is what gets frozen. - A stamp is not an edit:
updatedAtstays put, solatestStamp.stampedAt < skill.updatedAtis exactly "there are changes since the last stamp" — the detail page shows that as a badge next to the version. - Storage:
everdict_skills.version+everdict_skills.origin(jsonb) and theeverdict_skill_versionstable (mig 0091), behind theSkillVersionStoreport (kept out ofSkillStore: the row is read on every agent turn, the line only when someone opens the version panel — the same split asWorkspaceFs←FsRevisionStore).