ADR-0030: First-party components and how the legacy tool kinds run on them¶
- Status: Accepted
- Date: 2026-10-06
- Deciders: Jian (product owner)
Context¶
ADR-0025 said components live in a components/ directory in this repository, and ADR-0029 said the
legacy slack_post_message and gmail_send kinds are desugared to component calls and stay valid.
Building it (KAN-1886) needed four choices the earlier ADRs left open.
Decision¶
- First-party components ship inside the engine package, at
packages/engine/components/, not at the repository root. They must travel with the code that runs them: a root folder would not be in an installed@kampong/engine. The layout is the one ADR-0025 and KAN-1834 fixed (<namespace>/<name>/<version>/component.yaml), and thekampong/*namespace is served only from here.LayeredComponentRegistryanswerskampong/*from the first-party registry alone and everything else from the project's folder, so a project cannot shadow or impersonate a first-party component. - First-party components need no lockfile pin. They are versioned with the package that ships
them. A spec may still pin one with
kampong lock, and a pin that exists is enforced. Every other component still needs a pin whenrequirePinsis on. - Slack is a
kind: restmanifest; Gmail is akind: module. The Gmail API takes the whole message as base64url MIME, which a request template cannot build. The module sees only itsctxand may reach onlygmail.googleapis.com. - Legacy tools run as components only when a component dispatcher is configured.
desugarLegacyToolmaps the two kinds tokampong/[email protected]/kampong/[email protected]calls, keeping name, token,requires_approvalandextract. A bare engine, or an export that has not vendored the interpreter, takes the original request builders. The two paths produce byte-identical requests (tested against the legacy builders), so recorded fixtures keep matching.
Behaviour changes, deliberately¶
- Slack
{"ok": false}is a failure. Slack answers most errors with HTTP 200, and the legacy path reported them as success.kampong/slackdeclares afailure_whenrule, so the step now fails with Slack's error code. A flow that relied on ignoring a Slack error will now stop. - Gmail refuses a line break in
toorsubject. Those values are written into MIME header lines, so a line break in a step output could add headers such asBcc. The legacy builder had this flaw.
Consequences¶
- The two legacy builders in
http-tool.tsstay until every runtime that executes specs has components available; removing them is a later cleanup. - The canvas Component kind lists the first-party components next to the project's own.
- The exporter vendors the interpreter and carries the components a spec uses (see below), so exports run the same code paths.
Exports¶
- The vendored runtime gains
component.ts,component-core.ts(interfaces, the module runner, layering andStaticComponentRegistry),component-dispatch.tsandschema-validate.ts. The directory scan and the manifest parser stay in the engine; an export has no dependency on@kampong/spec, so it receives manifests already parsed and linted. exportProjecttakes the components the caller resolved (options.components) and writes each component's files byte for byte undercomponents/<id>/<version>/, plussrc/components.generated.tswith the manifests and digests baked in. It ships only what the spec uses (component tools, plus the first-party components behind legacy Slack and Gmail tools) and refuses a file path that could leave a component's directory.kampong exportresolves components the way a run does: a project component must be pinned inkampong.lockand match its pin, so an export never ships code no run was allowed to execute. First-party components need no pin.- An export does not re-verify
components/against the baked digests when it starts. That, a signed release and an SBOM are KAN-1837.