Marketplace
GET .../updates/{id}/diff and POST .../updates/{id}/merge accept skill and
agent updates. /diff returns a section-level diff (a skill’s unit is a ##
section of its SKILL.md; an agent’s is <file>::<## section> across its whole
instruction bundle); /merge takes a decisions map of section → "mine" |
"theirs". Skill diffs also return an opaque snapshotToken; the client must
send it with the merge so the server can reject a stale review if the local
skill, upstream bytes, or catalog version changed. /apply is the unreviewed
one-click landing and refuses team updates.
Both verbs require the update to still be open: like /apply, /merge answers
409 when the update is not pending or conflict, so a merge cannot be
replayed against one already applied or dismissed.
Merging a skill whose catalog item carries a bundle also re-materializes the
bundle’s references/, scripts/ and assets/ from the upstream commit, into
a new version-scoped directory that the row’s
metadata.catalogBundleInstallPath is repointed at in the same transaction —
so a file the upstream commit deleted is not delivered to agents afterwards. If
the catalog item has stopped carrying a bundle altogether, the pointer and file
inventory are cleared instead and the row returns to markdown_only. The
founder’s merged markdown is what lands in company_skills.markdown; the
bundle’s own SKILL.md is never written over it. The checkout runs before the
transaction, so a failed fetch leaves the pending update untouched and
retryable, and replacing an existing bundle directory is staged and renamed into
place rather than deleted up front — a failed fetch never leaves a skill with no
bundle. The response reports bundleMaterialized and, when a bundle was
written, bundleFileCount.
POST .../marketplace/crew/repair is founder-only. It diagnoses whether the
company’s AoA crew is inside the marketplace update pipeline and repairs it if
not — adopting …@legacy/unstamped crew agents in place, re-provisioning a
company that has no crew at all, or correcting an install-operation row that
reports failure over a committed crew. It returns { diagnosis, result } and is
a no-op on a healthy company.
Adoption is pointer-only: it rewrites templateOrigin and templateVersion
(to the 0.0.0-legacy sentinel) and installs the crew team’s company_skills,
and touches nothing else — instructions, skillKeys, runtimeConfig, triggers,
adapter and name all stay as the founder has them. The follow-on content update
then arrives through the company’s agentUpdatePolicy (auto-apply, or a
founder-visible pending update), so repair never discards founder edits. It is
all-or-nothing: if any roster member with a local agent row cannot be adopted,
nothing is written and the company stays repairable.
The same repair runs unattended as part of the boot/24h crew update pass, capped
per pass; the route exists for an operator who already knows a specific company
is stuck. Both share a 6-hour per-company cooldown — send {"force": true} to
override it after fixing the underlying cause.
POST /api/admin/marketplace/reconcile is an instance-admin recovery operation
with the mandatory strict body
{"scope":"fleet","mode":"repair","operationId":"<uuid>"}. It first performs a
fresh, deduplicated CDN catalog sync; a cache or bundled-snapshot fallback never
authorizes fleet mutation. Only a successful CDN attempt may drive the
full-fleet crew repair, legacy Steward adoption, crew update, and team-member
reconciliation sequence.
A 200 response has status: "success" | "partial", the caller-provided
operationId, executionDisposition: "started" | "joined_in_flight", catalog
identity, aggregate counters, typed skips[], operation diagnostics[], and
sanitized per-company failures[]. The deprecated replayed field remains for
one compatibility release and is true exactly when executionDisposition is
joined_in_flight. A matching concurrent request joins the same promise; a
different ID receives typed 409 operation_in_flight, and a completed ID is
never executed again.
Operation identity and state are stored in the instance-scoped
marketplace_reconciliation_operations ledger before the fleet is discovered.
A database-enforced singleton lease prevents different app replicas from
running overlapping fleet operations. The ledger remains authoritative across
restarts and for a zero-company fleet; company activity_log rows provide
per-company audit detail and cannot be created through the generic activity or
plugin logging APIs under the reserved reconciliation namespace.
Before catalog refresh or fleet mutation, the service records the sorted target
set with catalog: null. A successful completion audit records final catalog
identity and gives each company only its own skips and failures. A terminal
pre-mutation catalog failure remains inspectable. If mutation may have committed
but the completion audit fails, the POST returns
outcome_unknown_after_mutation; operators must inspect before deciding whether
another operation is safe.
Catalog refresh is side-effect-free with respect to installed marketplace
items. Reconciliation snapshots its target companies and persists the start
audit before running the catalog skill/plugin update check, so pending-update
rows and notifications cannot precede the audit boundary. A shared mutation
lock queues periodic/manual catalog update checks behind an in-flight audited
reconciliation, and the reconciliation update check receives the exact audited
company-ID snapshot owned by the ledger rather than rediscovering the fleet.
Every 400, 401, 403, 404, 409, 500, and 502 response uses the strict envelope
{ok:false,error:{code,message},operationId,retry,docUrl}. Raw catalog or
exception text is never returned. GET /api/admin/marketplace/reconciliations/{operationId} returns durable
running, success, partial, failed_before_mutation, or
outcome_unknown_after_mutation state plus safeToRetry and a typed recovery
instruction. Callers should retain the operation ID and follow that instruction;
do not infer retry safety from an HTTP timeout.
Pending-update persistence and notification errors from the crew-update pass
are included as sanitized crew_update failures rather than being reduced to
application-log-only warnings. The full operator workflow and code-to-recovery
table are in Marketplace recovery.
DELETE .../marketplace/teams/{teamId} is founder-only and permanently deletes
every agent on the team — except protected AoA agents (Commander, Steward),
which are detached rather than destroyed: the agent row and its triggers
survive, and only its team membership goes away with the team. The response
reports both sides, so retention is never silent:
parentProjectId is null), and roster edits —
both addMember and removeMember — are refused on a team with no parent
department, so a founder could neither detach the agent nor remove the team.
DELETE /api/companies/{companyId}/agents/{agentId} does refuse outright, with
409: deleting a single agent has an obvious alternative (pause it), so there
is no dead end.
Company Plugins
Mounted under/api/companies/{companyId}/plugins:
Instance Plugin Routes
{pluginId} must be the tenant-specific
plugins.id UUID. Manifest IDs/keys are deliberately rejected because the
same key can be installed by multiple companies. Plugin UI code can read the
UUID from PluginHostContext.pluginInstallationId.
Compatibility note (2026-08-02): integrations that previously stored a
manifest-key webhook URL must replace that segment with the installed plugin
UUID. There is no key fallback, because resolving a shared key would make an
unauthenticated webhook ambiguous across tenants. Existing UUID webhook URLs
continue to work unchanged.
Bridge routes under /api/plugins/{pluginId}/bridge/* and /api/plugins/{pluginId}/data/* are for plugin runtime/UI communication and enforce plugin capability and host checks.
Skills
PATCH .../skills/{skillId}/files, company_skills.customized is true and the
source-re-read paths refuse rather than replace it:
POST .../skills/{skillId}/install-update→ 409 withcode = "SKILL_CUSTOMIZED"at the top level (matching the catalog apply path) and the same value plusskillIdunderdetails. Nothing in the database or on disk is changed.POST .../skills/import→ 201 with the affected skills listed inrefusedCustomized(and a matching entry inwarnings); they are absent fromimported. Un-edited skills in the same import still update.POST .../skills/scan-projects→ 200 with the affected skills listed inconflicts(reason names the local edits); they are absent fromupdated. One edited skill never aborts the sweep.
POST .../skills only creates a fresh canonical key. A name/slug collision returns
409 with top-level code = "SKILL_NAME_TAKEN" and the same value plus slug and
key under details; the existing row and directory are untouched.
POST .../skills/import-package remains an authoritative authoring surface and clears
customized after replacing the bytes. Company bundle import is conservative instead:
an existing_company preview surfaces existingCustomized: true and skips that skill
by default. Replacing founder edits requires the exact manifest key in
overwriteCustomizedSkillKeys on both the preview and import requests.
A source-re-read refusal also raises a founder hub item. To take the upstream version,
delete the skill and re-import it. This mirrors the catalog apply path, which answers
409 SKILL_CUSTOMIZED and routes the founder to the diff/merge review instead.