0035: Integrations go through the vendor's CLI as an ordinary employee¶
Context and Problem Statement¶
Adapters implement capability protocols over any transport (ADR 0028).
The first two, GitHub and Atlassian, speak HTTP with a token the person creates by hand and stores through
Credentials (ADR 0030; ADR 0033
and ADR 0034 record how); the Google Workspace adapter would need an
OAuth client, which in a company only an administrator may register, and a Zoom adapter an app registration with its
own review. Every such step is a form, a ticket or an approval before hz drafts anything, and every token is a secret
hz must store, redact and rotate. Meanwhile the vendors ship command-line tools an ordinary employee may install and
log in to with their own account: gh for GitHub, acli for Atlassian, gws for Google Workspace. The maintainer
decided (2026-10-24): "Integrations use only what an ordinary employee may do. No admin-registered apps; vendor CLIs
(gh, acli, gws) are the transport and own authentication. Meetings come in as Zoom summary emails." How do adapters
reach a service, and who holds the credential?
Decision Drivers¶
- Perfect onboarding (INTENT.md, tenet 11): joining needs nothing an employee cannot do alone, and nothing an administrator must register or approve.
- Least privilege by construction: a CLI logged in as the person can do exactly what the person may do, no more, and the vendor's own consent screen said so.
- Fewer secrets in hz: a token hz never holds cannot leak through output, logs, receipts or a bug report (ADR 0027, ADR 0030).
- The vendors maintain login flows, token refresh, enterprise hosts and rate-limit handling in their CLIs; hz should not reimplement them per adapter.
- Tenets 8 and 9: no vendor lock-in and specifics as plugins; a company that reaches vendors differently binds its own providers.
Considered Options¶
- Vendor CLIs as the transport; the CLI owns authentication; no admin-registered apps (chosen)
- HTTP with a personal access token per service (the GitHub and Atlassian adapters today,
ADR 0033 and ADR 0034, which
weighed
ghas "an external program to install and authenticate"): works without an administrator for these two, but the person creates and pastes a token, hz stores and redacts it, Google and Zoom need registered apps anyway, and each adapter carries its own HTTP client, paging and error handling. - OAuth apps registered by the project (one hz client per vendor): the convenience of a browser login, but a company administrator must allow the app, Google's verification review applies to a restricted scope, and hz would hold refresh tokens for every service.
- MCP servers only: the vendors' hosted MCP servers also need an app or a gateway an administrator sets up; ADR 0028's generic MCP adapter stays for companies that run one.
- Vendor SDKs: the import-linter contract keeps vendor clients out of the core, and a plugin's SDK still needs a token or an app; nothing gained over the CLI.
- A hz service account or bot user: an administrator creates it, it sees more than any one person, and its token is the most valuable secret on the machine. Against least privilege and tenet 4 (humans own, with lineage).
Decision Outcome¶
Chosen option (proposed): where a vendor ships a CLI an ordinary employee may install and log in to, the adapter
uses it as its transport and the CLI owns authentication; hz stores no token for that service and needs no registered
app; Credentials stays for services without such a CLI. Checkable statements:
- The CLI is the transport and owns authentication. An adapter for GitHub runs
gh, for Atlassianacli, for Google Workspacegws. It never reads the CLI's token store, never calls a subcommand that prints a token, and declares noCredentialLetfor that service. Logging in is the vendor's flow (gh auth login,acli ... auth login,gws auth login), run by the person once; the adapter's setup step (ADR 0032) says which command and checks the result, nothing more. Whatever the vendor requires of an ordinary account for that login is the vendor's business and the adapter README's; hz adds no requirement of its own. - Only what an ordinary employee may do. No adapter in the core or a first-party plugin requires an administrator to register an application, create an OAuth client, grant an organisation-wide scope or create a service account. An adapter that can only work that way is not a first-party adapter; a company's private plugin may ship it.
- Invocation is bounded and shell-free. The core publishes the bounded subprocess helper the agent runners
already use (ADR 0029: argument lists without a shell, a process
group, SIGTERM then SIGKILL at the timeout, capped and drained stdout and stderr) for adapters. An adapter passes
an argument list, never a string; resolves the executable to an absolute path from its settings
(
[github] gh = "gh", looked up on PATH once per kernel, never from a projecthz.toml); gives every call a timeout (default 60 s, settable, at most the contract's); caps stdout (10 MiB by default) and keeps the last 64 KiB of stderr; closes stdin or passes exactly the bytes it means to; and runs with an environment allowlist (PATH, HOME, LANG, LC_*, TMPDIR, the CLI's own configuration variables the adapter names), neverHZ_CREDENTIAL_*and never the inherited environment whole. - Output is JSON, parsed into models. Every call asks the CLI for machine-readable output (
gh --json,acli --json,gwsJSON) and parses it into the pydantic models of the capability; unknown fields are ignored, every string passesclean_text(ADR 0028). Human-readable output is never parsed. Paging uses the CLI's own paging or limit flags until theWindowis covered or the byte budget is passed, then raises the "narrow the window" error of ADR 0028. - Errors are mapped, never relayed raw. A non-zero exit becomes a
HeinzelErrorthat names the operation, the exit code and what to do: not logged in or expired → the login command, reported throughattentionas adegradedcheck like a missing credential (ADR 0030); not found or forbidden → the resource, never the account; rate limited → when to retry; a timeout → the call and its limit; missing executable → the install command. The cleaned stderr tail may be logged at debug level only; it is never put in a draft, a receipt orhz system info. - Versions are checked, offline. An adapter declares the minimum CLI version it was written against.
hz system doctorruns<cli> --versionand nothing that reaches the network: missing or too old isdegradedwith the install or upgrade command; the CLI's local account record (gh auth status's hosts file,gws's stored account) is reported where the CLI exposes it without a request, otherwise doctor says "installed, login checked on first use". Whether a login still works is learned by the first call and reported throughattention(5). An adapter refuses to run a call against a CLI older than its minimum, naming the version. - Hosts come from the CLI's configuration. The adapter's
hostsfunction (ADR 0028) returns the host the CLI will talk to:gh's host setting (github.com or an Enterprise Server), the Atlassian site URL, Google's fixed hosts.hz system infoshows them as for every adapter. Credentialsstays for services without a CLI (ADR 0030), and for a CLI that itself needs a token in a variable (named in the adapter's settings, filled from a declaredCredentialLetfor that one call, never inherited). A service that has neither a CLI nor a token an employee may create is out of scope for first-party adapters (2).- Capabilities, providers and bindings are unchanged (ADR 0028).
The CLI is one transport behind a
ProviderLet; contracts still name capabilities, configuration still binds them,writestill reaches only publish operations. A company's private plugin may bind other providers for the same capabilities: its own wrapper CLI, an MCP gateway through the generic MCP adapter, or an HTTP adapter with a token it is allowed to hold. Such a plugin is typically the company layer of the record "layers and playbooks are plugins", installed with the team's stack; nothing here limits what a plugin may do with the user's rights, only what the core and first-party plugins require. - Not an isolation boundary. A CLI logged in as the person can do whatever the person may do; the adapter
calls read subcommands for
readmodes and the kernel grantswriteonly to publish operations, which is review's job to keep true, as for every adapter (ADR 0028, ADR 0030). Only install plugins you trust (SECURITY.md).
Consequences¶
- This record supersedes ADR 0033 and ADR 0034 (urllib, tokens).
- The GitHub adapter moves from its HTTP transport and the
github-tokencredential togh(gh apiwith--paginateand--jq, orgh pr list --json, as each method needs); the Atlassian adapter from HTTP and an API token toacli. TheirCredentialLets go, their READMEs say "install and log in",hz system doctorchecks the CLI, and their contract tests run against a fake CLI. The Google Workspace adapter starts ongws; no OAuth client of ours exists. Zoom gets no adapter and no app: meetings arrive as the meeting tool's summary e-mails through the mail capability, whose addition to ADR 0028 is in flight in a separate record; themeetingsprotocol stays for vendors with a CLI or a company plugin. Chat waits for a CLI an employee may use, or a company plugin. - Good: joining a team needs no ticket; the first win runs with the logins the person already has; hz holds no service token for GitHub, Atlassian or Google, so nothing to redact, rotate or leak.
- Good: enterprise hosts, SSO, token refresh and rate limits are the CLIs' problems; one bounded-subprocess helper replaces per-adapter HTTP clients and their redirect and redaction rules.
- Bad: a CLI on every machine the daemon runs on, found through PATH from a launchd or systemd environment; doctor and the adapter's setup step say what to install, and the version a plugin was written against pins the output shape.
- Bad: output shapes are the CLIs', not the APIs'; a CLI release can change them, and the version check is the only guard. Recorded fake-CLI outputs in tests must be refreshed against the real CLI deliberately.
- Bad: CI cannot log in; unit tests use recorded outputs, integration tests a fake executable written in Python (as ADR 0029), and the real CLIs run only on a maintainer's machine.
- Bad: rework of two adapters built this week, before they shipped.
Confirmation¶
Implemented for GitHub (gh, pull request #49) and Atlassian (acli, pull request #40): each CLI adapter's command
lines, JSON parsing, paging and error mapping against recorded outputs
(plugins/heinzel-github/tests/unit/gh_test.py, plugins/heinzel-atlassian/tests/unit/acli_test.py) and against a
fake CLI (plugins/heinzel-github/tests/integration/gh_test.py, plugins/heinzel-atlassian/tests/integration/acli_test.py).
Still to come with the shared helper and the Google Workspace adapter: the bounded subprocess helper's argument lists, timeouts, process group, output
caps and environment allowlist with real processes (tests/integration/runner_process_test.py, extended), and that an
adapter subprocess never sees HZ_CREDENTIAL_* or the inherited environment (tests/unit/kernel/process_test.py);
each CLI adapter's command lines, JSON parsing, paging, error mapping and version refusal against recorded outputs
(plugins/heinzel-<name>/tests/unit/*_test.py) and against a fake CLI (plugins/heinzel-<name>/tests/integration);
hz system doctor with the CLI missing, too old and present, without network (tests/integration/capability_test.py);
a repository test that no first-party adapter declares a CredentialLet for a service that has a CLI adapter, and
that no first-party plugin imports an HTTP client for such a service (tests/unit/repository_test.py); the
import-linter contract "The core imports no vendor client or agent SDK" (pyproject.toml), unchanged.
More Information¶
- Decided by the maintainer, 2026-10-24: "Integrations use only what an ordinary employee may do; vendor CLIs (gh,
acli, gws) are the transport and own authentication"; meetings as summary e-mails. Technical shape by the architect.
Accepted 2026-10-24, with both first-party adapters on their CLIs (Atlassian via
acli, #40; GitHub viagh, #49). - Builds on ADR 0028 (adapters over any transport,
hosts,clean_text, the window rule), ADR 0029 (the bounded subprocess the runners already have: process groups, timeouts, caps, environment allowlist), ADR 0030 (kept for services without a CLI;attentionfor an expired login), ADR 0032 (the adapter's setup step runs the vendor's login), ADR 0015 (doctor) and ADR 0017 (the adapters are first-party plugins). It does not change ADR 0028's text; the mail capability is being added to ADR 0028 separately and is referenced, not decided, here. - The onboarding record (pull request #32) relies on this one:
hz joindiscovers who the person is from the CLIs' own logins. The record "layers and playbooks are plugins" says where a company's own providers live: in the layers of its private plugin repository; the public reference company binds the first-party CLI adapters. - The validation slice (INTENT.md, "Thin slice first"): the weekly report draft reads merged pull requests through
gh, moved issues throughghoracli, and the week's meetings from summary e-mails throughgws; its first three Fridays decide whether the CLIs' output shapes and login state are stable enough in practice. - Open: whether the bounded subprocess helper is public API (
heinzel.run_bounded) or stays in the kernel's private modules with a public wrapper; the exact minimum versions; whethersystem.plugin's optional GitHub token (ADR 0030, known exception) also moves togh. Revisit when a vendor retires its CLI's JSON output or a company needs a transport no employee may install.