Go package boundaries
This page records decision B01: the ownership and import rules for every Go package in Open Conduit. The module is github.com/open-conduit/open-conduit, with Go 1.24.2 as its minimum. The boundary checker enforces these rules over the resolved production and test dependency graph as part of make check (development checks). A new package needs an explicit ownership entry here and in the checker before it can import anything.
| Package | Owns | Allowed project dependencies |
|---|---|---|
domain |
Five primitive records, identifiers, envelopes, revisions, lifecycle/outcomes and mechanical capabilities; opaque domain data | None |
extension |
Public ingress observations, deterministic route handlers, registration, toolbelt operations, executor and optional resource contracts | domain |
storage |
Public transactional guarantees, event/work/lease/timer/KV operations and separate artifact interface | domain |
observability |
Public lifecycle hooks, correlation and executor measurements | domain |
extension/inproc |
A way to write an extension as a Go library: the optional surfaces it offers and the host services it gets, which protocol/sdk/inprocsdk serves as a package. The host never uses it |
Public packages only; imported only by extensions/*, protocol/sdk and integration tests |
conformance |
Reusable public contract suites, fixtures, fake clock, synchronization barriers and reference doubles | Public packages above; no production implementations |
internal/runtime |
Durable event acceptance, observation diffing, routing serialization, scheduling, leases, retry, wait/wake, causation limits | Public packages above except conformance |
internal/runtime/runtimetest |
Test fixtures driving the production runtime over SQLite in a temp dir as conformance.RuntimeDrivers, used by the runtime’s own tests and integration/ |
Public packages, conformance (never conformance/reference), internal/runtime and SQLite; imported by tests only |
internal/storage/sqlite |
SQLite transactions, migrations and durability | storage, domain |
internal/artifacts |
Streaming local logs/outputs and retention implementation | storage, domain |
internal/backup |
Local SQLite/artifact snapshot composition, manifest verification and isolated restore | storage, domain, internal/storage/sqlite, internal/artifacts; never runtime or adapters |
internal/config |
Scope configuration loading, validation, secret resolution, reload snapshots | domain, extension |
internal/management |
Local authenticated management and toolbelt server, calling generic runtime operations | Public packages, internal/runtime, internal/config |
client |
Public local API client and wire types for operators/extensions | domain; public contracts where needed, never server internals |
extensiontest |
The test harness a package’s own repository uses: the real daemon, built from the pinned module and driven through client, a scope generated from the package’s manifest, and a scripted agent |
Public packages and client; never internal/ |
internal/telemetry |
Lifecycle export pipeline (validation, redaction, bounded buffering/drop accounting, per-exporter isolation), OpenTelemetry API mapping and local JSON log exporter | Public packages and the OpenTelemetry API; exporter configuration stays in extensions |
internal/cli, internal/tui |
Operator controls and unified history presentation (internal/cli/runview renders shared history rows) |
client, public packages; config editing through management/bootstrap services |
internal/web |
The browser view, a management client like the TUI | client, public packages and internal/cli |
extensions/{github,slack,jira,linear,bitbucket} |
Payload schemas, ingress reads/writes/markers, projections, domain routes and tool operations | Public packages and service SDKs; no runtime or SQLite imports |
extensions/tracker |
The shared tracker kit: ingress, route, projector, outbox and tools over a small Service |
Public packages only; imported only by extensions/linear, extensions/jira and extensions/bitbucket |
extensions/tracker/pkg |
The kit’s package entry point: serves a tracker over protocol version 1 (#428) | The kit, public packages and the protocol and SDK (not its examples); imported by extensions/linear/pkg and extensions/jira/pkg |
extensions/git |
Optional resource provider: shared/fresh workspace, copying, setup, cleanup | Public packages and process/filesystem utilities |
extensions/srt |
Optional process sandbox provider (B09) wrapping launches in srt; extensions/srt/pkg serves it as a bundled package |
Public packages and process/filesystem utilities |
extensions/docker, extensions/ssh |
Optional container providers: Docker containers behind a gateway, and runs on a remote over SSH; extensions/docker/pkg and extensions/ssh/pkg serve them as bundled packages |
Public packages and process/filesystem utilities |
extensions/tunnel |
Optional webhook tunnel providers (ngrok, cloudflared); extensions/tunnel/pkg serves each as a bundled package of that name |
Public packages and process utilities |
extensions/{schedule,introspection,observe,posthog,human} |
Scheduling rules, reporting/sweep/triage and the host watch (extensions/introspection/host), observed sources, telemetry export and human asks |
Public packages and service libraries; client if calling management |
extensions/shell, extensions/claudecode, extensions/codex (executors) |
Execution processes, adoption and executor-private continuity; CLI/MCP toolbelt presentation | Public packages, client, generic process helpers; no runtime/SQLite imports |
protocol |
The third-party subprocess protocol: version, manifest validation, method catalogue and error codes; wire types, error mapping and payload schema evolution | Public packages only |
protocol/jsonrpc, protocol/sdk, protocol/examples/* |
The transport, the minimal extension-side SDK, and the example external extension (notes) and executor (runner) with their commands |
Public packages and protocol/* only, exactly as a third party would |
internal/extproc |
Host runtime for extension processes: discovery, atomic conflicts, supervised processes, the scoped extension-to-host service and adapters onto the public contracts; install, update, rollback, removal and diagnostics | Public packages, protocol/* and internal/runtime; never SQLite, bootstrap or another extension |
internal/certify |
The extension conformance runner, shipped as open-conduit extension certify |
Public packages, protocol/*, conformance (never conformance/reference), internal/extproc, internal/runtime, SQLite and artifacts |
extensions/<name>/pkg |
First-party subprocess packages, beside the extension they serve, each with a Main that cmd/open-conduit runs as open-conduit extension-run <name> (no executable of their own): extensions/slack/pkg serves extensions/slack, extensions/github/pkg serves extensions/github unchanged through protocol/sdk/inprocsdk, extensions/shell/pkg and extensions/claudecode/pkg serve those executors the same way, and extensions/codex/pkg serves Codex, each over protocol/sdk; extensions/sentry and extensions/pagerduty are sources written only on protocol/sdk, with no in-process twin |
The extension’s own rule: public packages, client, protocol/* (not its examples) and its own directory |
internal/bootstrap, cmd/open-conduit, cmd/os |
Composition root and entrypoints; wire implementations and optional registrations | Packages required to assemble the binary; cmd/os uses client |
cmd/open-conduit-gateway |
The Docker gateway image’s program | Public packages and extensions/docker/gateway |
Arrows below mean “imports”; implementations point toward contracts. Bootstrap assembles runtime implementations; the local backup service composes only SQLite and artifact storage.
flowchart TD Bootstrap --> Runtime Bootstrap --> SQLite Bootstrap --> Extensions Bootstrap --> Executors Bootstrap --> Management Operators --> Client Management --> Runtime Runtime --> Contracts SQLite --> Contracts Extensions --> Contracts Executors --> Contracts Contracts --> DomainContracts represents extension, storage and observability. Public interfaces must not return concrete implementation types. Optional workspace context crosses the resource contract as an opaque host-bound ResourceBroker; Git acquisition semantics are B07. No Issue, PR, label enum, process session handle or Claude continuation type belongs in domain or internal/runtime. Snapshot diffing operates on opaque identities/revisions, not service payload fields. Retry/outcome decisions use generic policy; tracker-specific status mapping lives in projections.
Runtime must not import extensions, executors, client, management, config implementation, SQLite or Git workspace code, directly or transitively. Domain behavior belongs outside runtime. The executor owns session identity, resume-versus-fresh decisions, per-turn telemetry and opaque adoption state; runtime owns generic authority and execution history. Extensions use storage only through the public transaction/KV surface, never implementation tables. Resource consumers bind the public provider interface rather than importing extensions/git.
Enforcement
Section titled “Enforcement”The boundary checker traverses go list -deps -test -json output, including runtime tests, so test imports cannot smuggle in an adapter. Go’s internal rules alone do not enforce these boundaries. Implementation tests may import public contracts and the public conformance harness (including its public-contract dependency closure). Runtime tests may use that harness, but not conformance/reference, adapters or SQLite; real backend/workload composition belongs in integration/ outside runtime. Conformance suites run without runtime/SQLite and exercise opaque synthetic payloads; core slices run with no Git or service registration.
The checker also holds four source rules, the first two over the core, meaning the public contracts (domain, extension, storage, observability, protocol and their sub-packages) and internal/runtime (#354):
- No third-party modules. A core package imports only the standard library and the modules in the checker’s
thirdPartyallowlist, which is empty. It may not importdatabase/sqleither: drivers belong to the storage implementations. Composition may import anything. The cases are inTestCoreImportsNoThirdPartyModule. - No service, executor or workflow words. The core’s production source names no
github,slack,claude, issue, review or pull request in an identifier or string literal. Comments and import paths are not scanned, and neither are the identifiers that use “issue” as a verb for issued toolbelt tokens, whichverbIdentifierslists by file, so a tracker’sIssuetype elsewhere is still caught.vocabularyExceptionslists what a file still names today. It can only shrink: an entry that is no longer needed fails the check. The negative control isTestCoreVocabulary. - Composition imports no extension. No production file in
internal/bootstrapimports anextensions/*package: every extension runs as a package, and bootstrap composes only packages. Onlycmd/open-conduitimports the bundled ones, to serve them withopen-conduit extension-run. The negative control isTestCompositionImports. - Nothing registers an extension in process. Only an extension’s own code (
extensions/*), the SDKs that serve it (protocol/sdk, whoseinprocsdkserves anextension/inprocextension as a package) andintegration’s tests importextension/inproc, in production or tests. The negative control isTestInprocImports.
The setup extension is an installed package in its own module, open-conduit/extension-setup, pinned in go.mod with a tool directive and built only by scripts/bundle-packages.sh. No package may import it, or anything under extensions/setup again (boundary cases setup_package_never_imported, setup_package_never_imported_transitively, setup_never_compiled_back_in; #243).
extensiontest is imported by other modules’ tests, so it reaches only public packages and client (boundary cases extensiontest_uses_client, extensiontest_cannot_reach_internal; #259). It runs the daemon as a binary it builds, never in-process.
Composition may depend on the public conformance checks, because the certification runner ships in the open-conduit binary, but never on conformance/reference. The kill-drill fault injector in internal/bootstrap compiles only under the faultinject build tag.
Rules by package family
Section titled “Rules by package family”Command-line and terminal UI. Both commands share internal/cli: open-conduit operator commands and the os toolbelt CLI, each calling the management API only through client. The TUI is injected by the open-conduit entrypoint (cli.Env.TUI), so os carries no terminal UI. internal/cli reaches the composition root only through functions and values the entrypoint injects into cli.Env, and never imports an extension package or the TUI:
cli.Env.Serve:internal/bootstrap, the composition root behindopen-conduit serve.cli.Env.ValidateConfig: the daemon registry’s offline validation (bootstrap.ValidateConfig).cli.Env.Setup: the setup agent (bootstrap.SetupCommand).cli.Env.Extensions.Setups: the installed and bundled packages’open-conduit initflows (manifestsetup), each run in its own package oversetup.run; setup itself lives in the extensions.cli.Env.Adapters: the adapter catalogue, built from the installed and bundled packages’ manifests (fields), so the composition root names no extension for it.cli.Env.EditConfig: the config editor, implemented ininternal/tui.cli.Env.State: the offline state operations ininternal/bootstrap/state.go(recovery).
internal/tui may use internal/cli (claim and attach helpers) and stays free of executor and domain branches. The boundary checker’s tests name the core CLI/TUI cases explicitly.
Executors. An executor is an extension that declares one, so it lives under extensions/ too. extensions/shell imports only public contracts. extensions/claudecode (Claude Code executor) imports only public contracts. It keeps its own copy of the shell executor’s process-identity helpers, because executors cannot import each other. Executors reach workspaces only through extension.ResourceBroker and sandboxes only through extension.SandboxBroker, and runtime sees only those opaque brokers. The public extension.StopExecutor and extension.AttachExecutor carry physical-stop evidence and the operator attach control, so the daemon needs no private executor interface.
GitHub. extensions/github imports only public contracts. It holds the ingress, the engineering tool operations, the github-hold executor (which reaches the toolbelt through the public client) and extensions/github/playbooks (embedded playbooks and plugin preparation; the setup playbook is the setup extension’s own). Its extensions/github/githubtest REST fake is test support used by extension and integration tests, never by production code. extensions/github/pkg serves it as a bundled package.
Git workspaces. extensions/git (B07) imports only public contracts plus process/filesystem utilities. extensions/git/pkg serves it as a bundled package over protocol/sdk.
Webhook exposure. extensions/tunnel provides the ngrok and cloudflared tunnel types (extension.TunnelProvider), using the standard library only, and extensions/tunnel/pkg serves each as a bundled package of that name over tunnel.run. A static public URL needs no provider.
Slack. extensions/slack (Slack extension) uses public contracts plus the standard library, including its own minimal WebSocket client for Socket Mode, so it adds no module. Its extensions/slack/slacktest fake is test support only. extensions/slack/playbooks holds the slack-thread playbook, which bootstrap writes into the Claude Code plugin beside the GitHub playbooks. extensions/slack/pkg serves it as a bundled package; no Git or workspace code is involved.
Scheduled jobs. extensions/schedule (scheduled jobs) uses public contracts plus github.com/robfig/cron/v3. It does not import extensions/github: it reads the accepted raw github.delivery Events itself and maps them into its Delivery, and its route handler, timer recurrence, job Work kind policy and webhook mount are extension/inproc surfaces served by extensions/schedule/pkg. GitHub job waits resolve through the host’s WaitTarget service, which the schedule package answers with its job wait target, so neither extension imports the other.
Introspection. extensions/introspection (issue reporting and introspection) imports domain, extension, storage and observability (the telemetry exporter), plus github.com/robfig/cron/v3 for its calendar. Its playbooks are in extensions/introspection/playbooks. The host watch is the sub-package extensions/introspection/host (using golang.org/x/sys/unix): it is an optional introspection feature whose crossings are reports, not a separate extension, and runs inside the introspection package (extensions/introspection/pkg). Introspection imports no other extension: agents log notes through its own introspection.correction and introspection.problem, and it writes only its own namespaces. No core package imports it. A boundary case and a source scan (TestNoPrivilegedReportingPath) keep reporting, introspection, triage, sweep slot, fix request and host watch vocabulary out of core, contract and operator packages.
Trackers. extensions/tracker, the shared tracker kit (installed integrations), provides ingress, route, projector, outbox and tools over a small Service, on public contracts only (plus extensions/tracker/playbooks). extensions/linear, extensions/jira and extensions/bitbucket may import it; no other extension may, and it reaches no extension (boundary cases trackers_use_kit, kit_cannot_reach_github, slack_cannot_use_kit, linear_cannot_reach_jira, runtime_cannot_import_kit). The kit’s package entry point, extensions/tracker/pkg, may also use the protocol and SDK to serve a tracker as a package; the kit itself may not (kit_package_serves_protocol, kit_cannot_import_sdk, kit_package_cannot_use_examples). Their lineartest, jiratest and bitbuckettest fakes are test support and the packages’ certification fixtures. extensions/posthog imports domain, observability, extension/inproc, protocol and the standard library.
Extensions written on extension/inproc. extension/inproc is public, so an extension implements it with public contracts only (boundary cases extensions_implement_inproc, inproc_cannot_reach_runtime). It is how most first-party extensions are written, not how they run: protocol/sdk/inprocsdk serves each as a package. Every first-party extension and executor (shell, claude-code, posthog, human, observe, linear, jira, bitbucket, schedule, introspection, slack, github and codex), the srt sandbox, git workspace, docker and ssh container providers and the ngrok and cloudflared tunnels run only as packages bundled with the release (open-conduit extension-run NAME). internal/bootstrap/compose.go composes each surface by iterating the running packages, bundled and installed alike (issues #66, #70, #357, #428, #477).
Extension protocol. The public protocol package (extension protocol) may import only public contracts, and extension may not import it. protocol/jsonrpc, protocol/sdk, the examples under protocol/examples and internal/extproc (runtime, trust model and schema evolution) each have their own checker rule. The public conformance suite conformance.RunExtensionKV covers the namespaced KV an extension reaches through the host. The examples’ binaries are built by the integration tests and are not part of the shipped binaries. protocol/examples/weakened holds the certification negative controls, under the protocol/* rule.
The transport comparison lives in prototypes/transport, a separate Go module (with its own go.mod) so go-plugin and gRPC stay out of the main module. ./... patterns, make check and the boundary checker do not include it; make protocol-prototype runs it.
Extension packages. internal/certify has its own checker rule; a first-party package lives in its extension’s directory and follows the extension rule. A package may use the public management client, as the implementation it wraps may (GitHub’s hold executor reaches the toolbelt through it). The daemon loads installed packages through internal/bootstrap/extensions.go (extension packaging, SDK and certification). make packages builds the package directories; make certify runs the runner and the negative controls.
Non-goals
Section titled “Non-goals”The core has no Effect primitive or outbox state machine, DAG, workflow language or remote management. Adapters reconcile writes and mark messages whose effects cannot be re-derived. Namespaced KV is mandatory coordination storage, not a second event log or artifact warehouse.
Related decisions
Section titled “Related decisions”These decisions preserve the boundaries above:
- lifecycle and storage fencing: B02;
- ingress completeness and push overlap: B03;
- ordering, wait races and active policy: B04;
- config inheritance, reload and authentication: B05;
- the workspace contract: B07;
- generic injection acknowledgement: runtime semantics;
- pending job coalescing: runtime semantics;
- the subprocess protocol: B08.
History
Section titled “History”- 01-01 recorded B01 as the ownership and import rules for the interfaces and scaffold built in 01-02 to 01-06, under the heading “Planned Go package boundaries”.
- 01-02 implemented
domain,storage, the shared storage cases inconformanceand a volatile double inconformance/reference. It introduced the module with Go 1.24.2 as its minimum to run those contracts. - 01-03 added
extension,observabilityand their conformance fixtures (B03). - 01-06 added the boundary checker, the remaining scaffold and CI, and reviewed the supported toolchain and platforms (contracts gate).
- 02-07 added the
ostoolbelt CLI tointernal/cliandexecutors/shell, on public contracts only. - 02-08 moved the TUI behind
cli.Env.TUI, injected by theopen-conduitentrypoint. - 02-09 added
internal/bootstrap, the composition root behindopen-conduit serve, reached byinternal/clionly throughcli.Env.Serve, with the kill-drill fault injector under thefaultinjectbuild tag. The 02-09 and 05-04 gates ran a core slice with no Git or service registration. - 03-01 added
extensions/githubon public contracts, with itsgithubtestfake, registered byinternal/bootstrap. - 03-02 added
extensions/git(B07). - 03-03 added
executors/claudecodewith its own copy of the process-identity helpers. - 03-04 added the engineering tool operations, the
github-holdexecutor andextensions/github/playbookstoextensions/github. - 03-05 added
extensions/tunnel, and the injectedcli.Env.ValidateConfigandcli.Env.Setup. - 03-08 added
extensions/introspectionfor issue reporting ondomain,extensionandstorage, with its triage playbook and theTestNoPrivilegedReportingPathsource scan. - 04-01 added
extensions/slackand itsslacktestfake. - 04-02 moved setup into the extensions (
cli.Env.Setups), named the core CLI/TUI cases explicitly in the checker’s tests, and addedextensions/slack/playbooks. - 05-01 added
extensions/scheduleon public contracts plusrobfig/cron. 05-02 kept it there by resolving GitHub job waits through a bootstrap-injected function. - 05-03 completed
extensions/introspection: it addedobservabilityandrobfig/cronto its imports and theextensions/introspection/hostsub-package. The separateextensions/hostpackage in the original plan was folded into it. The source scan was extended to sweep slots, fix requests and the host watch config. - 06-01 added
extensions/tracker, its boundary cases, the Linear, Jira and Bitbucket fakes,extensions/posthog,cli.Env.Adaptersandcli.Env.EditConfig. - 06-02 added the offline state operations in
internal/bootstrap/state.go, reached throughcli.Env.State, andinternal/migratefor explicit config conversion, with its own checker rule. - Stages 01–06 non-goals. The original non-goals also excluded a third-party extension protocol and manifest. Subprocess transport and schema evolution were deferred to 07-01 to 07-03, after shipped workload evidence.
- 07-01 added the public
protocolpackage with a checker rule and four cases, and theprototypes/transportmodule. The daemon’s private stop interface and its claude-code attach branch were replaced by the publicextension.StopExecutorandextension.AttachExecutor. - 07-02 added
protocol/jsonrpc,protocol/sdk, the examples andinternal/extproc, with two checker rules and six cases, andconformance.RunExtensionKV. At that point bootstrap did not yet load extensions. - 07-03 added
internal/certifyandpackages/{slack,shell}with checker rules and seven cases,protocol/examples/weakenedand the daemon wiring ininternal/bootstrap/extensions.go. Composition could then depend on the publicconformancechecks. - #428 moved
executors/*andpackages/*intoextensions/<name>/: an executor is an extension that declares one, and a first-party package’s library (pkg/) sits beside the extension it serves. The extension rule now admitsprotocol/*(not its examples), and the separate package rule went away. The same issue madecmd/open-conduitserve every first-party package asopen-conduit extension-run <name>, so they build no executables of their own.