Every workload and every piece of catalog in this system is a Kubernetes custom
resource in the group core.controller-agent.dev/v1alpha1. They are all
namespaced. The CRD definitions ship in the core-controller chart's
install-only crds/ directory (generated from
controllers/core-controller/api/v1alpha1/*_types.go), so installing that chart
registers every kind below.
Two kinds of actor reconcile them:
- The
core-controlleroperator (controllers/core-controller) watches most kinds. OnlyToolRunandAgentRuncreate Kubernetes Jobs;Corpuscreates a sync CronJob and a cronEventSourcea tick CronJob; every other controller only validates the spec and sets aReadycondition. - Out-of-cluster brokers. The
connection-brokerreadsConnection/Corpusfor ingestion and retrieval; themcp-brokerownsMCPServer/MCPToolentirely (there is no in-cluster controller for those two — see ADR 0045);integration-gatewayreadsEventSources to serve webhooks. Theagent-orchestratorand the Temporalcatalog-syncprocess read the catalog kinds into a vector store for RAG retrieval.
Most kinds are operator-authored (hand-deployed catalog / gitops). Three are
system-written: ToolRun and AgentRun (one per invocation, created by the
orchestrator) and MCPTool (derived and owned by the mcp-broker). Skill is
usually operator-authored but is also derived from each KnowledgeBase.
| Kind | Purpose | Authored by | Acted on by | Defining ADR |
|---|---|---|---|---|
| Tool | A catalog capability: a container Job or an agent-backed tool | operator | tool_controller + orchestrator | 0010 |
| LocalTool | A catalog tool run as packaged code in an in-pod sidecar | operator | localtool_controller + orchestrator | 0014 |
| ToolRun | One tool invocation → a Job | system (orchestrator) | toolrun_controller | 0010 |
| Agent | A full agent loop launched as a Job | operator | agent_controller + orchestrator | 0002 |
| AgentRun | One agent invocation → a Job | system (orchestrator) | agentrun_controller | 0021 |
| Skill | Trusted markdown + an allowed tool/agent set | operator (or derived) | skill_controller + orchestrator | 0008 |
| Connection | An authenticated route to one external system | operator | connection_controller + connection-broker | 0043 |
| Corpus | One scoped subset of a Connection, indexed for RAG | operator | corpus_controller + connection-broker | 0043 |
| KnowledgeBase | A queryable composition of Corpora | operator | knowledgebase_controller + orchestrator | 0039 |
| MCPServer | An authenticated route to one MCP server | operator | mcp-broker (no in-cluster controller) | 0045 |
| MCPTool | A proxied MCP tool, materialized into the catalog | system (mcp-broker) | mcp-broker (no in-cluster controller) | 0045 |
| EventSource | How events arrive (verified webhook or cron) and how far they are trusted | operator | eventsource_controller + integration-gateway | 0048 |
| EventTrigger | CEL match on an event → orchestrator / Skill / Agent / Tool, with params + replies | operator | eventtrigger_controller + orchestrator (both engines) | 0048 |
| IdentityProvider | How a per-user credential is resolved and injected | operator | identityprovider_controller + orchestrator | 0032 |
graph TD
subgraph exec["Tools & agents"]
Tool; LocalTool; Agent; Skill
ToolRun; AgentRun
end
subgraph kb["Knowledge base"]
Connection; Corpus; KnowledgeBase
end
subgraph mcp["MCP"]
MCPServer; MCPTool
end
subgraph events["Events"]
EventSource; EventTrigger
end
IdentityProvider
Skill -->|toolRefs| Tool
Skill -->|agentRefs| Agent
Agent -->|skillRefs| Skill
Agent -->|toolRefs| Tool
Tool -->|agentRef| Agent
ToolRun -->|toolRef| Tool
AgentRun -->|agentRef| Agent
Corpus -->|connectionRef| Connection
KnowledgeBase -->|corpusRefs| Corpus
KnowledgeBase -.->|derives a| Skill
MCPServer -->|discovers & owns| MCPTool
MCPTool -.->|joins the catalog, referenced by| Skill
EventTrigger -->|sourceRef| EventSource
EventTrigger -->|skillRef| Skill
EventTrigger -->|agentRef| Agent
EventTrigger -->|toolRef| Tool
Tool -->|identityProviders| IdentityProvider
Agent -->|identityProviders| IdentityProvider
Connection -->|identityProviders| IdentityProvider
MCPServer -->|identityProviders| IdentityProvider
A catalog entry for an invokable capability — either a container tool
(image + serviceAccountName, launched as a ToolRun Job) or an
agent-backed tool (agentRef, dispatched as an AgentRun). Supersedes the
former JS manifest.json (ADR 0009,
0010).
- Authored by: operator.
- Key spec fields:
description(string, required) — embedded for RAG tool retrieval.input/output(string, required) — the argv/stdin contract and result shape.allowedRoles([]string, required, ≥1) — RBAC retrieval filter; the caller needs at least one.tier(string) — cost/trust class.agentRef(string) — names anAgentthis tool wraps.image+serviceAccountName(string) — container launch target (the SA must pre-exist).args/env/secretEnv/resources/timeoutSeconds— Job container config;secretEnvsources from Secret keys, never literals.identityProviders([]string) —IdentityProviderCRs the caller must have linked.- CEL: exactly one of
agentRefor (imageandserviceAccountName).
- Status:
Readycondition. - Referenced by:
Skill.toolRefs,Agent.toolRefs,EventTrigger.target.toolRef,ToolRun.toolRef. - Acted on by:
tool_controller.go(validates the launch target); orchestrator (retrieval + dispatch). - Sample:
config/samples/tool_v1alpha1_tool.yaml.
Like a Tool, but it points at packaged code pulled from a language registry at
runtime and run by a per-language executor sidecar in the orchestrator pod —
never a Job (ADR 0014). Creating one
is privileged (it fetches and runs third-party code): gate it with k8s RBAC.
- Authored by: operator (privileged).
- Key spec fields:
description/input/output/allowedRoles(≥1) /tier— same catalog semantics asTool.runtime(string, required, enumnode;python;go;shell) — selects the executor sidecar.package+version— registry coordinate and exact pinned version (ranges/tags are rejected fail-closed); required for node/python/go.entry— module/console-script/binary when it differs from the package default.sourceURL+checksum— required forshell(no registry); sha256 verified before run.env/secretEnv(orchestrator resolves secrets and passes them over the pod-local unix socket),resources,timeoutSeconds.network(bool, default false) — opt-in egress; otherwise the sidecar unshares the network namespace.
- Status:
Readycondition (spec validity only). - Acted on by:
localtool_controller.go(validates cross-field packaging constraints); the orchestrator's per-language sidecars execute it. - Sample: none.
One tool invocation. The orchestrator creates a ToolRun instead of a Job
directly, so the controller owns all Job creation and RBAC
(ADR 0010).
- Authored by: system — created by the orchestrator per invocation.
- Key spec fields:
toolRef(string, required) — theToolto launch.args([]string) — appended after the Tool's static args.callback(required) — exactly one delivery mode: HTTP (url+ HMACsecretRef) or NATS (natsSubject+natsUrl).secretEnv([]SecretEnvVar) — per-invocation secrets, merged over the Tool's (run entry wins on a name clash) — e.g. a per-user token.timeoutSeconds(int32, default 300).
- Status:
phase(enumPending;Running;Succeeded;Failed, derived from the owned Job),jobName,startTime,completionTime,message,conditions. - Acted on by:
toolrun_controller.go(owns abatch/v1Job). - Sample:
config/samples/tool_v1alpha1_toolrun.yaml.
A full agent loop launched as a one-shot Job — same execution architecture as a
Tool, but given a natural-language goal at runtime and iterating internally
before reporting a final answer over the callback protocol. Its catalog half
mirrors Tool so agents join the same RBAC-scoped retrieval
(ADR 0002, 0021).
- Authored by: operator.
- Key spec fields:
description/input/output/allowedRoles(≥1) /tier— catalog fields.image+serviceAccountName(required; SA must pre-exist).orchestratorPrompt— guidance for the parent planner about when to delegate here.agentPrompt— the system prompt the sub-agent's own loop runs with.skillRefs([]string) —SkillCRs this agent may load.toolRefs([]string) —ToolCRs this agent's own loop may call (ADR 0028).model(advisory),maxIterations,env/secretEnv/resources,initContainers.identityProviders([]string).
- Status:
Readycondition. - Referenced by:
Tool.agentRef,Skill.agentRefs,EventTrigger.target.agentRef,AgentRun.agentRef. - Acted on by:
agent_controller.go; launched viaAgentRunby the orchestrator. - Sample:
config/samples/tool_v1alpha1_agent.yaml.
One agent invocation — the ToolRun pattern, but the payload is a
natural-language goal and the target is an Agent.
- Authored by: system — created by the orchestrator (or another caller).
- Key spec fields:
agentRef(required),goal(string, required),callback(required, reuses the ToolRun HMAC/NATS protocol),secretEnv(merged over the Agent's),timeoutSeconds. - Status: same shape as
ToolRun—phase,jobName, timings,message,conditions. - Acted on by:
agentrun_controller.go(owns abatch/v1Job). - Sample:
config/samples/tool_v1alpha1_agentrun.yaml.
Hand-authored, trusted markdown plus an allowed tool/agent set
(ADR 0008). A Skill is never run as a
pod; its markdown is injected as system-prompt context. It deliberately has no
allowedRoles — its audience is derived as the intersection of its
referenced tools' and agents' roles (ADR 0011).
- Authored by: operator — and also derived from each
KnowledgeBase(ADR 0039 §2). - Key spec fields:
description(required) — RAG retrieval.markdown(string, required) — trusted system-prompt content.toolRefs/agentRefs([]string) — the tools it may invoke and agents it may delegate to; the planner's choice is re-validated against these.allowCallerTools(*bool) — governs caller-supplied tools (ADR 0035); unset means allowed (the OpenAI wire-contract default).
- Status:
Readycondition. - Referenced by:
Agent.skillRefs,EventTrigger.target.skillRef. - Acted on by:
skill_controller.go(validates refs); the planner consumes the markdown. - Sample:
config/samples/tool_v1alpha1_skill.yaml.
The knowledge-base subsystem is a three-tier split
(ADR 0038,
0043): a Connection is the
credentialed route, a Corpus is a scoped subset of it, and a
KnowledgeBase composes Corpora into the thing an agent queries. All three
live in one namespace with their credential Secrets; the connection-broker (not
the in-cluster controllers) does the actual listing, fetching and per-user
retrieval.
An authenticated route to one external system — one per Slack workspace / Confluence site / Drive account, not per channel or space. Holds the provider, address and ingestion credential.
- Authored by: operator (holds the ingestion credential).
- Key spec fields:
provider(string, required, enumconfluence;slack;gdrive).site—baseURL(required,^https://, used for citations only) +cloudId(required for Confluence custom domains).allowedScopes— an allowlist capping which subsets its Corpora may reach (spaces/channels/folderIDs).autoJoin(bool, slack-only) — the credential self-joins a channel on a refused read (a write).secretEnv— the shared-service ingestion credential.identityProviders([]string) — per-user retrieval delegation; copied into each Corpus status.- CEL: confluence ⇒
siterequired;autoJoinonly for slack.
- Status:
corpora(count of referencing Corpora — makes deletion refusable),observedGeneration,Readycondition. - Referenced by:
Corpus.connectionRef. - Acted on by:
connection_controller.go(ref-counts Corpora, blocks deletion while any remain);connection-broker. - Sample:
config/samples/core_v1alpha1_connection.yaml(+_slack,_gdrive).
One scoped subset of a Connection — this Confluence space, this Slack channel, this Drive folder. Several per provider are normal; each owns one vector-store collection, stamped with the Corpus's roles at ingest.
- Authored by: operator; status fields are controller-written.
- Key spec fields:
connectionRef(string, required) — the Connection it draws from.description(required);displayName(defaults to the name; distinct within a KB for citations).allowedRoles([]string, required, ≥1) — RBAC, stamped on every chunk.scope(required) — exactly one ofspace/channel/folderID; the security boundary.sync—mode(enumwebhook;poll;none),reconcileInterval,backfill.since.api— the live GET face:enabled(default false),methods(enumGET).- CEL: exactly one scope unit;
sync.reconcileIntervalrequired unlessmode: none.
- Status:
provider+identityProviders(copied from the Connection),collection,resources(indexed count),lastSyncTime,lastReconcileTime,webhook,Ready+Syncedconditions. - Referenced by:
KnowledgeBase.corpusRefs. - Acted on by:
corpus_controller.go(owns a sync CronJob);connection-broker+ the vector-store indexer. - Sample:
config/samples/core_v1alpha1_corpus.yaml(+_slack,_gdrive).
Composes multiple Corpora into the queryable unit an agent asks (e.g. a client
engagement) — many-to-many. The indexer derives a Skill from it whose tools
are its search/fetch tools plus each member's GET face. Like Skill it has no
allowedRoles: its audience is the union of its members, enforced by
per-chunk role filtering at the vector store (ADR 0039 §4).
- Authored by: operator; status counts are controller-written.
- Key spec fields:
description(required) — the subject matter, so the planner can tell knowledge bases apart.displayName,aliases(alternate names, embedded).corpusRefs([]string, required, ≥1, set) — the member Corpora (an explicit list, not a selector).chunk— defaults formaxTokens(800) andoverlap(100).disclosePartialVisibility(*bool, default true) — report how many members were withheld by role.- CEL:
chunk.overlap < chunk.maxTokens.
- Status:
documents(total),perCorpus(per-member counts + sync times),staleCorpora,missingCorpora(dangling refs),Readycondition. - Acted on by:
knowledgebase_controller.go(aggregates counts/staleness); the indexer/orchestrator consume the derived Skill. - Sample:
config/samples/core_v1alpha1_knowledgebase.yaml.
MCP support (ADR 0045) exposes a Model
Context Protocol server's tools without ever giving the agent loop an MCP
client. The mcp-broker is the only component that speaks MCP: it discovers a
server's tools and materializes the exposed ones as ordinary catalog tools. These
two kinds have no in-cluster controller — the broker reconciles them.
An authenticated route to one MCP server. Operator-authored and privileged: it introduces an externally-controlled call path reachable by agents.
- Authored by: operator (privileged);
statusis written by themcp-broker. - Key spec fields:
transport(string, required, enumstreamable-http).url(string, required,^https?://) — the broker's endpoint;httpsfor an external server,httppermitted for a cluster-internal one.secretEnv— the shared-service discovery credential (fortools/list).identityProviders([]string) — the per-user invocation credential, presented ontools/call; fails closed, never falling back to the discovery credential.exposure([]MCPToolExposure, default-deny, keyed byremoteToolName) — each entry:remoteToolName(required),expose(*bool, default true),allowedRoles(required, ≥1),toolID(optional, must be a DNS-1123 name),hidden,tier.
- Status:
discoveredTools(the full advertised inventory, each markedexposed),exposedTools(count),Ready/Degraded conditions. - Owns: the derived
MCPToolCRs (viaownerReferences, so deletion cascades). - Acted on by: the
mcp-broker— there is no in-cluster controller. - Sample:
config/samples/core_v1alpha1_mcpserver.yaml.
The catalog record a remote MCP tool becomes — an ordinary tool whose dispatch
kind is mcpExec, carrying what the broker needs to proxy one tools/call.
Derived — do not edit: the mcp-broker writes and owns these from an
MCPServer's exposure map and reconciles any hand-edit back.
- Authored by: system — the
mcp-broker(owner-referenced to itsMCPServer). - Key spec fields:
serverRef(required),remoteToolName(required),description(required, RAG),inputSchema(the remote JSON Schema, verbatim),allowedRoles(required, ≥1, copied from the exposure entry),hidden,tier,identityProviders(copied from the server). - Status:
Readycondition (the owning server still advertises this remote tool; the broker deletes it otherwise — the §6 existence rule). - Referenced by:
Skill.toolRefs/Agent.toolRefs, by its catalog id (like any tool). - Acted on by: the
mcp-broker— no in-cluster controller. - Sample:
config/samples/core_v1alpha1_mcptool.yaml.
Event-driven dispatch is two kinds split by trust, like MCPServer/MCPTool
and Connection/Corpus: the privileged, credential-bearing source and
the many cheap triggers that read from it
(ADR 0048). The implementation contract —
envelope, CEL subset, matching, /invoke shape, gateway endpoints, cron — is
docs/event-triggers.md. Both kinds live in the release
namespace.
How events arrive and how far they are trusted. The object name is the route:
a webhook source is served by integration-gateway at POST /webhooks/<name>;
a cron source becomes a core-controller-owned CronJob that ticks
POST /events/<name>/tick. Every source produces the same normalized envelope
(source, kind, id, time, headers, body, sender, repository).
- Authored by: operator (privileged: it names the verification Secret).
- Key spec fields (exactly one of
webhook/cron):webhook.verify—scheme(hmacSha256|slackV0|bearer; there is no "none": unauthenticated is not a source),header(required forhmacSha256;bearerdefaults toauthorization),prefix(stripped before the hex compare),secretRef({name, key}, same namespace).webhook.deliveryIdHeader— becomesenvelope.id(else a uuid).cron—schedule(5-field),timeZone,suspend,payload(any JSON object, delivered asenvelope.body).sender(webhook only) —provider: github, CELloginFrom/isBotFrom/repositoryFrom,dropBots(defaulttrue). A login is only forwarded as the turn's principal after the provider verifies it; a source withoutsenderruns as the gateway's service identity.
- Status:
Readycondition; cron sources alsostatus.cronJobName(eventsource-<name>). A cron source isReady=False(TickNotConfigured) when core-controller has noEVENT_TICK_URL. - Acted on by:
eventsource_controller.go(validates, owns the cronCronJob);integration-gatewayserves and verifies webhooks from it. - Sample:
config/samples/core_v1alpha1_eventsource.yaml.
apiVersion: core.controller-agent.dev/v1alpha1
kind: EventSource
metadata: { name: github } # POST /webhooks/github
spec:
webhook:
verify:
scheme: hmacSha256
header: x-hub-signature-256
prefix: "sha256="
secretRef: { name: integration-gateway-secrets, key: GITHUB_WEBHOOK_SECRET }
deliveryIdHeader: x-github-delivery
sender:
provider: github
loginFrom: body.sender.login
isBotFrom: body.sender.type == "Bot"
repositoryFrom: body.repository.full_name
---
apiVersion: core.controller-agent.dev/v1alpha1
kind: EventSource
metadata: { name: nightly-sweep }
spec:
cron:
schedule: "0 7 * * *"
payload: { repo: imaustink/agent-controller, limit: 25 }What a matching event dispatches. The orchestrator (both engines) evaluates every
Ready trigger's CEL against the envelope; the highest priority (then name)
wins, and no match is a clean no-op (200 {"status":"ignored"}), never a RAG
fallback.
- Authored by: operator.
- Key spec fields:
sourceRef— restricts to one EventSource;priority— tie-break.match.cel— aboolCEL expression over the envelope (absent = every event fromsourceRef). A compile failure isReady=False; an evaluation error is a non-match.params— name → CEL, evaluated to typed values; rendered into every{{name}}template.sessionKey— template for the turn's session id (elseevent:<source>:<id>).target— CEL: exactly one oforchestrator: {}(ordinary conversational turn),skillRef,agentRef,toolRef(runs the Tool once withparamsas its input, skipping the planner);promptTemplate(required unlesstoolRef);readOnly— the deterministic write gate, carried onto everyAgentRun/ToolRunthe turn launches.reply.github—repository,issueNumber,ack(+sessionPage),result,removeLabel(on every outcome, durably).reply.http— a literalurl(never templated) + optional signingsecretRef. Executed byintegration-gateway.
- Status:
Readycondition (CEL compiles, refs resolve). - Acted on by:
eventtrigger_controller.go(compile + ref check); agent-orchestrator and the Temporal gateway match and dispatch. - Sample:
config/samples/core_v1alpha1_eventtrigger.yaml; the shipped examples are bare manifests inexamples/event-triggers/(not part of any chart).
apiVersion: core.controller-agent.dev/v1alpha1
kind: EventTrigger
metadata: { name: github-pr-review }
spec:
sourceRef: github
match:
cel: >-
headers["x-github-event"] == "pull_request" &&
body.action == "labeled" && body.label.name == "ai-review"
params:
repository: body.repository.full_name
number: body.pull_request.number
label: body.label.name
sessionKey: "github:{{repository}}#{{number}}"
target:
agentRef: claude-code-swe-agent
readOnly: true
promptTemplate: Review pull request {{repository}}#{{number}} ...
reply:
github:
repository: "{{repository}}"
issueNumber: "{{number}}"
ack: "🤖 Starting work on this now."
sessionPage: true
result: true
removeLabel: "{{label}}"Declares, once per provider (e.g. github), how the orchestrator resolves and
injects that provider's per-user credential. Replaced a hardcoded map in the
orchestrator so this is cluster config, watched like the catalog kinds
(ADR 0032).
- Authored by: operator (cluster config).
- Key spec fields:
envVar(string, required,^[A-Z][A-Z0-9_]*$) — the env var the resolved token is injected as on a run'ssecretEnv.label(string, required) — human-facing name in link prompts; unique per namespace.flow(enumoauth;claude-cli-setup-token;claude-remote-login, defaultoauth) — which link flow backs it (ADR 0027).crossEntryPoint(bool) — key the credential by principal vs raw entry-point subject (ADR 0030 §6).
- Status:
conditions. - Referenced by:
Tool,Agent,Connection,MCPServer,MCPTool— all viaidentityProviders. - Acted on by:
identityprovider_controller.go(uniqueness check only); the orchestrator reads it. - Sample: none.
- Edit the Go type in
controllers/core-controller/api/v1alpha1/<kind>_types.go(the doc comments on each field are the source of truth for this reference). - Run
make manifests generateincontrollers/core-controller— this regenerates the CRD YAML, the deepcopy methods, and syncs the CRD into the chart'scrds/automatically. - The chart's hand-maintained RBAC (
charts/.../rbac.yamlfor each process that watches the kind) is not generated — update it in the same change, and bump the umbrellaChart.yamlversion so Argo re-renders. - Keep this reference in step with the new or changed fields.