Skip to main content

Implementation guidance

This appendix provides practical guidance for implementing Courier interpreters. The guidance in this section is non-normative and doesn’t introduce new requirements beyond those defined in the Courier Interpreter RFC.

Any implementation that correctly adheres to the interpreter contract defined by the RFC is valid and supported, regardless of language or framework. The patterns described here exist to improve consistency, safety, and long-term maintainability across the Courier ecosystem.

Process model

Courier interpreters are executed as single, bounded invocations. Each invocation is responsible for performing exactly one job step and then terminating.

Key characteristics of the process model include:

  • Single invocation guarantee
    An interpreter is invoked once during each job step. It must not assume reuse across steps or executions.

  • STDIN handling
    Structured input is provided to the interpreter through standard input. Interpreters should treat STDIN as the authoritative source of invocation data and must not rely on environment variables or side channels for required inputs.

  • STDOUT and STDERR handling
    Interpreters may emit diagnostic information to standard output and standard error. These streams are intended for logging and troubleshooting only and aren’t part of the interpreter’s contractual output.

  • Structured result emission
    Contractual output must be emitted in the structured form defined by the RFC. Logs and diagnostic output must not be interleaved with structured results.

  • Process lifecycle responsibility
    Interpreters are responsible for managing the lifecycle of any processes they spawn. If an interpreter is terminated—whether due to timeout, cancellation, or failure—it must make a best-effort attempt to terminate any child processes or managed execution it has started. Interpreters must not intentionally leave orphaned processes running beyond the lifetime of the job step whenever it’s possible to prevent this.

Interpreters must terminate promptly after producing results. Background execution or long-running processes are explicitly discouraged.

Engine integration patterns

Interpreters may integrate with execution engines in different ways. Regardless of the pattern used, interpreters must treat the underlying engine as an internal dependency and must not leak engine-specific behavior into the interpreter contract.

Where an execution engine is available as a Habitat package, interpreters should strongly prefer using the Habitat-packaged version to ensure consistency, reproducibility, and predictable runtime behavior. Any changes to this approach must be explicitly documented.

CLI invocation

CLI-backed interpreters invoke one or more command-line tools to perform work.

Recommended practices include:

  • Prefer executing the CLI through a Habitat package when one is available
  • Avoid relying on system-installed binaries or environment-specific paths
  • Treat the CLI as an internal dependency, not a user-facing interface
  • Capture and normalize exit status, stdout, and stderr
  • Translate CLI behavior into structured, contract-defined output

Using a Habitat-packaged CLI provides controlled versions, consistent dependencies, and predictable behavior across environments. If a Habitat package isn’t used, the interpreter must document the rationale and any implications for portability or supportability.

API invocation

API-backed interpreters interact with external systems through APIs.

Recommended practices include:

  • Manage authentication and authorization internally
  • Normalize API responses into stable, interpreter-defined output
  • Absorb API versioning and protocol changes
  • Avoid exposing API-specific fields or semantics directly in the contract

API-backed interpreters are well-suited for SaaS integrations and remote systems.

Runtime embedding

Runtime-backed interpreters execute logic within an embedded runtime environment.

Recommended practices include:

  • Prefer embedding runtimes or tools provided through Habitat packages when applicable
  • Avoid relying on system-level runtimes or ad-hoc dependency installation
  • Isolate runtime dependencies from the host environment
  • Provide deterministic behavior across supported platforms

As with CLI-backed interpreters, the use of Habitat-packaged runtimes improves portability, repeatability, and long-term maintainability. If an embedded runtime isn’t sourced from a Habitat package, this decision must be documented along with any operational trade-offs.

Regardless of the integration pattern, engine selection and dependency management are implementation details. The interpreter contract must remain stable and independent of these choices.

Testing and validation

Testing interpreters requires more than verifying that execution succeeds. Interpreters must be tested at the contract and behavior level, independent of the full Courier ecosystem.

Recommended testing practices include:

  • Contract tests
    Verify that valid inputs are accepted, invalid inputs are rejected, and structured output conforms to the documented contract. Contract tests should exercise boundary conditions and validation failures explicitly.

  • Behavior tests
    Validate interpreter behavior across expected scenarios, including success, partial success, failure, retries, and interruption. Tests should focus on observable behavior rather than internal implementation details.

  • Regression protection
    Protect against accidental changes in contract meaning, output structure, or failure semantics. Regression tests should fail when contract behavior changes unintentionally.

To support efficient testing, implementers are encouraged to use the Courier Runner testing tool to simulate interpreter execution without requiring a full Courier deployment. This allows interpreters to be exercised in isolation while preserving realistic invocation, input, and output behavior.

Using the Runner testing tool helps ensure that interpreters behave correctly when executed by Courier and reduces the cost and complexity of local development and CI pipelines.

Mocking external dependencies and commands

Interpreters often depend on external systems, commands, or services. To achieve reliable test coverage, these dependencies should be mocked or isolated wherever possible.

Recommended practices include:

  • Mocking external APIs and services during tests
  • Stubbing or simulating CLI commands rather than invoking real binaries
  • Providing fake or controlled engine responses to exercise edge cases
  • Verifying interpreter behavior under failure conditions that are difficult to reproduce reliably in real systems

Mocking allows tests to be deterministic, fast, and safe, and enables validation of error handling, retry guidance, and partial success scenarios.

While integration tests against real engines are valuable, they should complement (not replace) contract and behavior tests. Interpreters must be testable without requiring live infrastructure, credentials, or external system availability.

By isolating external dependencies, implementers can improve test reliability, increase coverage, and catch contract regressions early.

Preferred implementation frameworks

To promote consistency and reduce implementation overhead, Courier provides Go-based interpreter scaffolding projects that implement common patterns and best practices.

These scaffolding frameworks:

  • Handle interpreter lifecycle and IO correctly
  • Enforce structured input and output handling
  • Provide helpers for validation, error reporting, and artifact management
  • Reduce boilerplate and common implementation mistakes

Where appropriate, implementers are encouraged to use:

  • The GoLang interpreter scaffolding project, or
  • The GoLang Copilot interpreter skeleton project

These frameworks represent the preferred starting point for new interpreter development.

However, use of these frameworks isn’t required. Any interpreter implementation that correctly implements the contract and execution semantics defined in the Courier Interpreter RFC is acceptable and supported, regardless of language or framework choice.

The contract is what matters. Frameworks exist to make correct implementations easier, not to constrain innovation.

Portability and future evolution

Interpreter implementations should be designed with portability and future evolution in mind.

Recommended practices include:

  • Avoiding reliance on platform-specific behavior where possible
  • Isolating engine-specific logic behind clear internal boundaries
  • Treating contract evolution as a deliberate, versioned process

Interpreters that follow these practices are easier to maintain, easier to migrate, and safer to depend on over time.

Thank you for your feedback!

×