02 — Architecture
The repo is a content distribution graph that compiles to two runtimes. Once installed, an agent's actual runtime topology depends on which surface you deployed it on:
- Cowork / Claude Code — single Claude session reads the plugin files locally; system prompt + skills are dispatched on-demand by the agent loop.
- Claude Managed Agents — the deploy script normalizes the same files into a
POST /v1/agentsbody (with subagents POSTed first) so the orchestrator runs as a depth-1 hierarchy on Anthropic infra; the firm's workflow engine drives steering and inter-agent handoffs.
Both surfaces consume the same source-of-truth files.
High-level component map
flowchart LR
subgraph Source["Source-of-truth (one repo)"]
direction TB
VP["plugins/vertical-plugins/<br/>(skills, commands, MCP configs)"]
AP["plugins/agent-plugins/<br/>(self-contained agents)"]
CB["managed-agent-cookbooks/<br/>(agent.yaml, subagents/, steering examples)"]
PB["plugins/partner-built/<br/>(LSEG, S&P Global)"]
M["marketplace.json"]
VP -- "skills propagated by<br/>scripts/sync-agent-skills.py" --> AP
AP -- "system: file:<br/>skills: from_plugin:" --> CB
M --> AP
M --> VP
M --> PB
end
subgraph Cowork["Cowork / Claude Code (interactive)"]
direction TB
CL["Claude session"]
SK["Skills auto-fired"]
CMD["Slash commands"]
MCP1["MCP HTTP servers"]
CL -- triggers --> SK
CL -- /comps, /dcf --> CMD
CL -- tool_use --> MCP1
end
subgraph CMA["Claude Managed Agents (headless)"]
direction TB
DH["scripts/deploy-managed-agent.sh"]
SUB["subagents POSTed first"]
ORCH["orchestrator POSTed last"]
SESS["session.steer(...)"]
OL["scripts/orchestrate.py<br/>(reference event loop)"]
HO["handoff_request<br/>{target_agent, payload}"]
DH --> SUB --> ORCH
SESS --> ORCH
ORCH --> HO
HO --> OL
OL -- routes to --> SESS
end
Source -. installs .-> Cowork
Source -. deploys via API .-> CMAThree plugin tiers
| Tier | Lives in | Self-contained? | Purpose |
|---|---|---|---|
| Vertical plugin | plugins/vertical-plugins/<vertical>/ |
Yes — own plugin.json, commands/, skills/, .mcp.json, hooks/ |
Source of truth for skills + slash commands + MCP connectors. Install on its own to get /comps, /dcf, etc. |
| Agent plugin | plugins/agent-plugins/<slug>/ |
Yes — bundles a synced copy of every skill it needs | The interactive form of a named workflow agent. Loadable into Cowork. |
| Partner plugin | plugins/partner-built/<vendor>/ |
Yes | Third-party (LSEG, S&P Global) bundles |
A skill — say dcf-model — therefore lives in two places:
- Source:
plugins/vertical-plugins/financial-analysis/skills/dcf-model/SKILL.md - Bundled copy:
plugins/agent-plugins/{pitch-agent,model-builder,...}/skills/dcf-model/SKILL.md
These must be byte-equal; scripts/check.py:114-131 enforces it via filecmp.dircmp. To change a skill, you edit the source and run scripts/sync-agent-skills.py, which shutil.rmtrees each bundled copy and shutil.copytrees the source in (sync-agent-skills.py:35-37).
How a managed-agent cookbook references the plugin
flowchart TB
yaml["managed-agent-cookbooks/pitch-agent/agent.yaml"]
sysmd["plugins/agent-plugins/pitch-agent/agents/pitch-agent.md"]
skills["plugins/agent-plugins/pitch-agent/skills/* (bundle)"]
vskills["plugins/vertical-plugins/*/skills/* (source)"]
reader["./subagents/researcher.yaml"]
modeler["./subagents/modeler.yaml"]
deck["./subagents/deck-writer.yaml"]
yaml -- "system: file: ../../plugins/agent-plugins/pitch-agent/agents/pitch-agent.md" --> sysmd
yaml -- "skills: from_plugin: ../../plugins/agent-plugins/pitch-agent" --> skills
yaml -- "callable_agents: manifest:" --> reader
yaml -- " " --> modeler
yaml -- " " --> deck
vskills -. "synced into" .-> skillsThe agent.yaml therefore has no embedded prompt and no bundled skill content — it is purely a reference document. The deploy script inlines them at POST time (deploy-managed-agent.sh:114-130):
inline_system() {
...
if jq -e '.system | type == "object"' >/dev/null <<<"$json"; then
sysfile=$(jq -r '.system.file // empty' <<<"$json")
...
body="$(cat "$base/$sysfile")"
[[ -n "$append" ]] && body="${body}"$'\n\n'"${append}"
jq --arg s "$body" '.system=$s' <<<"$json"
fi
}This is why one source produces two surfaces: the cowork plugin reads the same .md interactively; the managed-agent variant inlines it into the POST body.
Managed-agent isolation tiers (the load-bearing pattern)
The most opinionated piece of the architecture. Every cookbook README has a "Security & handoffs" tier table. The shape is constant; the labels vary by agent. From managed-agent-cookbooks/gl-reconciler/README.md:24-31:
| Tier | Touches untrusted docs? | Tools | Connectors |
|---|---|---|---|
reader |
Yes | Read, Grep only |
None |
| Orchestrator | No | Read, Grep, Glob, Agent |
Read-only GL + subledger MCPs |
resolver (Write-holder) |
No | Read, Write, Edit |
None |
The invariants:
- Exactly one worker holds
Write. The "write-holder" never reads outsider documents. Writes land in./out/. - Untrusted-document readers have no MCP servers, no Bash, no Write — their only output channel is a JSON blob constrained by an
output_schema:(subagents/*.yaml). - The orchestrator never reads outsider documents directly — it dispatches and aggregates.
- A separate
criticre-verifies reader output against trusted MCPs before the resolver consumes it (ingl-reconcilerandearnings-reviewer).
This is structural: the system prompt asks for safe behaviour, but the tools list makes unsafe behaviour impossible.
Per-agent topologies (depth-1)
┌────────────────── orchestrator (read-only MCPs, Agent)
│
┌──────────┴──────────────────┐
│ │ │
reader critic resolver
(untrusted) (re-verify) (Write)
schema-out trusted MCPs no untrusted IO
Variants seen in the cookbooks:
| Cookbook | Untrusted reader | Trusted re-verifier | Write-holder | Notes |
|---|---|---|---|---|
pitch-agent |
n/a (data is from CapIQ/Daloopa) | n/a | deck-writer |
Task-decomposition split: researcher, modeler, deck-writer |
gl-reconciler |
reader |
critic |
resolver |
+ Bash sandbox not needed |
kyc-screener |
doc-reader |
(rules-engine) | escalator |
UBO chart parser is read-only |
earnings-reviewer |
transcript-reader |
(model-updater) | note-writer |
Coverage list fan-out via session-per-ticker |
meeting-prep-agent |
news-reader |
profiler |
pack-writer |
Briefing pack output |
model-builder |
data-puller |
auditor |
builder |
The builder also has Bash (sandboxed) for openpyxl |
month-end-closer |
ledger-reader |
(rollforward) | poster |
Receives breaks from gl-reconciler |
statement-auditor |
statement-reader |
reconciler |
flagger |
Pre-distribution LP-statement QC |
valuation-reviewer |
(gp-package reader) | (template runner) | (LP report stager) | GP packages from outside |
market-researcher |
sector-reader |
comps-spreader |
note-writer |
Optional .pptx |
The depth-1 invariant is asserted in CI: scripts/test-cookbooks.sh:14-17 rejects any subagent that itself defines callable_agents, and any leak of output_schema into a deployed body.
Cross-agent handoffs
Agents can request work from each other via a literal blob in the orchestrator's text output:
{"type":"handoff_request","target_agent":"<slug>","payload":{"event":"...","context_ref":"..."}}
The reference event loop in scripts/orchestrate.py watches message_delta events on the source session, regex-extracts handoff JSON (orchestrate.py:40-42), validates it (extract_handoff, orchestrate.py:45-61), and steer()s the target agent's session.
sequenceDiagram
participant Source as Source agent (e.g. gl-reconciler)
participant Loop as scripts/orchestrate.py
participant Target as Target agent (e.g. month-end-closer)
Source->>Loop: SSE message_delta containing<br/>{"type":"handoff_request",...}
Loop->>Loop: HANDOFF_RE.search(text)
Loop->>Loop: target ∈ ALLOWED_TARGETS?
Loop->>Loop: jsonschema.validate(payload)
Loop->>Target: client.beta.agents.sessions.steer(<br/>agent_id, input=payload.event)
Target-->>Source: (separate session, runs to completion)There is a deliberate security caveat in the script header (orchestrate.py:8-15): handoff text is downstream of untrusted-document readers, so a malicious document could attempt to inject a handoff blob via the orchestrator's echo channel. Mitigations:
ALLOWED_TARGETSallowlist of deployed slugs (orchestrate.py:23-27).HANDOFF_PAYLOAD_SCHEMAcapseventlength and restrictscontext_refto^[A-Za-z0-9 ._/:#-]+$(orchestrate.py:29-38).- The script is explicitly labelled "REFERENCE ONLY" — production should "prefer emitting handoffs via a dedicated tool call or a typed SSE event the model cannot produce by quoting document text."
Data flow — single agent run (managed-agent)
Concrete example: gl-reconciler reconciling a trade date.
sequenceDiagram
participant WE as Workflow engine (Temporal/Airflow)
participant API as POST /v1/agents/.../sessions/.../steer
participant Orch as gl-reconciler (orchestrator)
participant Reader as gl-reconciler-reader (untrusted)
participant Critic as gl-reconciler-critic
participant Resolver as gl-reconciler-resolver (Write)
participant GLMCP as internal-gl MCP
participant SubMCP as subledger MCP
participant Custodian as Custodian PDFs
WE->>API: steer event: "Reconcile GL vs subledger,<br/>trade date 2026-04-30, classes: ..."
API->>Orch: input
Orch->>GLMCP: read GL balances (read-only)
Orch->>SubMCP: read subledger balances (read-only)
Orch->>Reader: dispatch per asset class
Reader->>Custodian: Read counterparty/custodian statements
Reader->>Reader: extract candidate breaks
Reader-->>Orch: structured JSON (length-capped, char-class restricted)
Orch->>Critic: re-verify each break against trusted MCPs
Critic->>GLMCP: confirm balances
Critic->>SubMCP: confirm balances
Critic-->>Orch: confirmed/rejected per break
Orch->>Resolver: verified break set
Resolver->>Resolver: render exception report (xlsx-author skill)
Resolver-->>Orch: ./out/exception-report.xlsx written
Orch-->>API: final message (may include handoff_request to month-end-closer)Key abstractions / interfaces
| Abstraction | Defined in | Shape |
|---|---|---|
| Plugin | plugins/*/.claude-plugin/plugin.json |
{name, version, description, author} |
| Marketplace | .claude-plugin/marketplace.json |
{name, owner, plugins: [{name, source, description}]} |
| Agent system prompt | plugins/agent-plugins/<slug>/agents/<slug>.md |
YAML frontmatter (name, description, tools) + markdown body |
| Skill | plugins/vertical-plugins/<v>/skills/<s>/SKILL.md (and bundled) |
YAML frontmatter (name, description) + markdown body; may carry helper scripts |
| Slash command | plugins/vertical-plugins/<v>/commands/<c>.md |
YAML frontmatter (description, argument-hint) + markdown body |
| MCP connector set | plugins/vertical-plugins/financial-analysis/.mcp.json |
{mcpServers: {<name>: {type, url}}} |
| Managed-agent manifest (orchestrator) | managed-agent-cookbooks/<slug>/agent.yaml |
{name, model, system, tools, mcp_servers, skills, callable_agents} |
| Subagent manifest | managed-agent-cookbooks/<slug>/subagents/<sub>.yaml |
Same shape minus callable_agents, plus optional output_schema |
| Steering events | managed-agent-cookbooks/<slug>/steering-examples.json |
[{event, description}, ...] — example free-text inputs |
| Handoff request | Embedded JSON in orchestrator output | {type:"handoff_request", target_agent, payload:{event, context_ref?}} |
Microsoft 365 add-in tooling — separate sub-architecture
The claude-for-msft-365-install/ plugin is unrelated to the agent plugins. It is a Claude Code plugin (not Cowork) whose six commands walk an enterprise admin through:
- Picking a path:
gateway/vertex/bedrock/foundry(commands/setup.md:36-44). - Provisioning the cloud-side resource (OAuth client, IAM role, Foundry resource).
- Granting Azure admin consent (one-click URL).
- Generating the customized add-in
manifest.xmlviascripts/build-manifest.mjs. - Optionally writing per-user routing config via Microsoft Graph extension attributes.
- Optionally building a "bootstrap endpoint" for per-user MCP servers + skills.
It is the on-ramp for getting the Claude Office add-in deployed in a tenant, after which the agent plugins are what runs inside it.