box

package
v0.0.0-...-3238171 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 20, 2026 License: MIT Imports: 30 Imported by: 0

Documentation

Index

Constants

View Source
const (
	LabelKey        = "coop"            // every coop box: coop=box
	LabelBox        = "box"             //   (its value)
	LabelSupervised = "coop.supervised" // a supervised inner box (build/update restart it): =1
	LabelOn         = "1"               //   (its value)
	LabelSupervisor = "coop.sup"        // value=<supervisor id>, so a supervisor kills only its own
	LabelFork       = "coop.fork"       // readable value=<fork name> for runtime diagnostics
	LabelForkOwner  = "coop.fork-owner" // repo-scoped value, so stop never reaps another repo's namesake
	LabelRun        = "coop.run"        // value=<loop run id>, so cancellation reaps the daemon-owned box
	// LabelHost records the HOST PROCESS supervising this box, as
	// v1:<workspace-scope>:<pid>:<start-token> — the versioned form parseSupervisorLabel reads
	// back. Every other label above names a LOGICAL owner (a run, a supervisor id, a fork); none of
	// them says which process would have removed the box, so nothing could decide whether a box left
	// behind by a SIGKILLed coop is an orphan. Cleanup is pull-only (--rm never fires on SIGKILL),
	// and this is what a later invocation in the SAME workspace pulls on: see SurveyOrphanBoxes.
	LabelHost = "coop.host"
	// DescendantsDrainedExit and DescendantsTimedOutExit are emitted only by coop-entry in an
	// opted-in supervised run. Keep them outside ordinary provider conventions and remap raw
	// provider use in the entrypoint before the host sees it.
	DescendantsDrainedExit  = 190
	DescendantsTimedOutExit = 191
)

Container labels coop stamps on its boxes so it can find and tear them down later. The SET sites (assembleArgs, below) and the cli QUERY sites (CountByLabel/KillByLabel) MUST agree — a label renamed on only one side would orphan running containers — so both reference these.

View Source
const CoopIgnoreFile = ".coopignore"

CoopIgnoreFile is the repo-local file listing extra paths to shadow, one per line (# comments and blank lines ignored). It extends the SecretGlobs denylist with project-specific secrets the defaults can't know about.

Variables

View Source
var AllowGlobs = []string{
	"cacerts.pem", "cacert.pem", "ca-bundle.pem", "ca-bundle.crt", "ca-certificates.crt", "ca-cert.pem",
}

AllowGlobs are EXACT, known-PUBLIC filenames that stay visible even though they match a secret pattern: the well-known CA bundles that `*.pem`/`*.crt` would otherwise shadow — emptying a trusted CA bundle breaks TLS verification inside the box (e.g. Elixir's castore at deps/castore/priv/cacerts.pem). These are specific public files, so they override even a high-confidence key pattern. An explicit .coopignore entry is still authoritative and can re-hide one.

View Source
var SecretGlobs = []string{
	".env", ".env.*", ".envrc", "*.secret", "*.secrets",
	"*.tfvars", "*.tfvars.json", "*.tfstate", "*.tfstate.*",
	"*.pem", "*.key", "*.p12", "*.pfx", "*.jks", "*.keystore", "*.p8", "*.ppk", "*.kdbx", "*.ovpn", "*.pkcs12",
	"id_rsa*", "id_ed25519*", "id_ecdsa*", "id_dsa*",
	".netrc", "_netrc", ".npmrc", ".yarnrc", ".yarnrc.yml", ".pypirc", ".git-credentials", ".htpasswd",
	".dockercfg", ".pgpass", ".my.cnf", ".s3cfg", ".boto", ".vault-token", "vault-token",
	"secrets", ".secrets", "credentials", ".aws", ".kube", ".ssh", ".gnupg", ".docker",

	"credentials.json", "service_account.json", "service-account.json", "*-sa.json", "client_secret*.json",
	"firebase-adminsdk*.json", "gha-creds-*.json", "auth.json", "secret.json", "secrets.json", "*.secret.json",
	"kubeconfig", "kubeconfig.yaml", "kubeconfig.yml", "database.yml",

	"credentials.y*ml", "secrets.y*ml",
}

SecretGlobs are filename patterns that must never enter the box. A match is shadowed: a directory becomes an empty tmpfs, a file an empty read-only decoy. Matching is by basename, at any depth (the repo's .git is always skipped).

This is a denylist: it catches well-known credential names, NOT every secret a given repo might hold (e.g. a token in an app config under a custom name). Add repo-specific paths in a .coopignore (see CoopIgnoreFile / LoadUserGlobs). Matching is case-insensitive (see NewShadowDecider), so .ENV / ID_RSA can't slip past.

Functions

func AuthedAgents

func AuthedAgents(cfg *config.Config) []string

AuthedAgents returns agents whose active account looks authenticated. The active account is the configured default unless a concrete run selected another stored profile; a provider-wide env token counts only in the default slot. This is a presence heuristic, not a live validity check.

func BaseDockerfile

func BaseDockerfile() string

BaseDockerfile is the shared base image: Node, the agent CLIs + ACP adapters (each agent names its own npm packages), and asdf — so the box honors a repo's .tool-versions at runtime, with no per-project Dockerfile needed. It runs as the non-root `node` user and is built from stdin, so the base never needs a checkout.

func BaseImageSkew

func BaseImageSkew(cfg *config.Config, img string) (builtBy string, skewed bool)

BaseImageSkew reports whether the base image was built from a different box definition than this binary carries (e.g. `coop update --self-only` without the rebuild), and by which coop version. Best-effort: no stamp (image built by an older coop, or elsewhere) reads as no skew — never nag on a guess.

func Build

func Build(rt runtime.Runtime, cfg *config.Config, repo string, fresh bool, version string) error

Build builds the box image: a repo with a .agent/Dockerfile builds that (its own toolchain), otherwise the shared base is built from BaseDockerfile. When fresh is set it adds --pull --no-cache so the base image and the npm-installed agent CLIs + ACP adapters are pulled to their latest (this is `coop update`). version is the building coop's version, stamped beside the image so a later launch can flag binary/image skew (box can't resolve it itself — cli owns it).

func BuildWith

func BuildWith(rt runtime.Runtime, cfg *config.Config, repo string, fresh bool, version string, stdin io.Reader, stdout io.Writer) error

BuildWith is Build with the runtime's own stdin/stdout supplied by the caller. `coop acp` speaks JSON-RPC over os.Stdout and reads the editor's requests from os.Stdin, so a build on that path must NOT touch either: build chatter on stdout corrupts the wire, and reading stdin swallows the editor's initialize. It passes an empty reader and os.Stderr instead (ui.* is already stderr-only, so progress lands in the editor's agent log either way).

func ComposeFile

func ComposeFile(workspace, policyRepo string) string

ComposeFile returns the sibling-services compose file, or "" if it's absent or empty (a zero-byte file declares no services). It completes the config-source/runtime-identity split: the relative PATH (box.compose, else .agent/compose.yml) is trusted config, read from policyRepo, while the FILE itself is read from the workspace at that path — so a fork uses the parent's committed choice of WHERE the compose file lives, but its OWN copy of the file. For a plain repo pass repo twice.

func ComposeFileAt

func ComposeFileAt(workspace, relativePath string) string

ComposeFileAt returns the tracked compose file selected by a trusted project policy. The relative path has already been validated by project.Load; this helper keeps review-specific dependency stacks bound to the disposable candidate rather than the parent checkout.

func ComposeProject

func ComposeProject(workspacePath string) string

ComposeProject is the per-WORKSPACE compose project (and network) name: the sanitized basename plus a short hash of the workspace's CANONICAL path. Distinct per checkout, so a fork and its parent — or two clones — never share one compose project (and its volumes). The path is canonicalized (symlinks resolved, e.g. macOS /var→/private/var) so the SAME physical workspace always yields the SAME name: its sidecar volumes persist across every run. Distinct from the image tag (ServicesProject), which stays repo-based.

func DownServices

func DownServices(rt runtime.Runtime, workspace, policyRepo string, volumes bool, stdout, stderr io.Writer) error

DownServices stops the current workspace's hashed Compose project and reconciles a positively owned basename-era project. Volumes are optional for the current project only: legacy volumes carry no workspace ownership label, so Coop never removes them automatically.

func DownServicesFile

func DownServicesFile(rt runtime.Runtime, workspace, file string, volumes bool, stdout, stderr io.Writer) error

DownServicesFile is the explicit-file counterpart to EnsureServicesFile. Review runs use it to remove their short-lived project, network, and volumes before the disposable candidate goes away.

func EffectiveProfiles

func EffectiveProfiles(cfg *config.Config, agent string) []string

EffectiveProfiles returns stored profile directories plus one synthetic env-backed default when needed. Consumers that mutate credential directories must continue to use Config.Profiles.

func EnsureProfilesDir

func EnsureProfilesDir(cfg *config.Config, agent string) error

EnsureProfilesDir creates agent's profiles/ dir (0700) if it's missing — run before a profile other than the default is created, so config.AgentProfileDir resolves "default" (and every named profile) under it. Idempotent: a no-op once profiles/ exists.

func EnsureServices

func EnsureServices(rt runtime.Runtime, workspace, policyRepo string, stdout, stderr io.Writer) ([]string, error)

EnsureServices brings the repo's sibling services up (compose up -d --wait) so a box can reach them by name. It is idempotent — already-running services are a fast no-op — and a no-op (nil, nil) when the repo has no compose file. On success it returns the non-empty service names in Compose's resolved order, from the same project and file selection it started. Progress is written to stdout/stderr; the caller decides where to point them and gates on a compose-capable runtime (Apple `container` has no compose). Shared by `coop up` and box.Run's auto-start.

func EnsureServicesFile

func EnsureServicesFile(rt runtime.Runtime, workspace, file string, stdout, stderr io.Writer) ([]string, error)

EnsureServicesFile is the explicit-file form used by trusted review policy. The file must live inside workspace; ValidateComposeFile enforces that its bind mounts cannot escape that boundary.

func EnvFileValues

func EnvFileValues(path string) map[string]string

EnvFileValues resolves an env file into the KEY→value pairs a box started from it will actually see. A bare KEY imports the ambient value (and, when there is none to import, leaves an earlier assignment standing rather than clearing it), and later duplicate assignments win, matching the runtime env-file contract. Comments, blanks, an empty key, and a missing file contribute nothing.

Exported because a value in here is not only the box's: the host resolves a bearer token from this same file when it has to inline one for an agent that cannot read the file itself (internal/sessionsvc, ACP mcpServers). One parser, so the two can never disagree about what the box has.

func ImageBuildAge

func ImageBuildAge(cfg *config.Config, img string) (time.Time, bool)

ImageBuildAge returns when img was last built by this coop install, from the mtime of whichever stamp the build wrote (base meta, or a per-project inputs hash). ok is false when no stamp exists — the image wasn't built here, so its age is a guess.

func ImageExists

func ImageExists(rt runtime.Runtime, image string) bool

ImageExists reports whether the given image is present locally.

func ImageForRepo

func ImageForRepo(repo, baseImage, override string) string

ImageForRepo decides which image a repo runs in: an explicit override wins; a repo with its own box Dockerfile gets its own tag (so a project's toolchain never clobbers the shared base); everything else uses the base image.

func NewShadowDecider

func NewShadowDecider(repo string) func(relSlash string) bool

NewShadowDecider returns a predicate reporting whether a repo-relative slash path is shadowed from the box: its basename matches SecretGlobs (and AllowGlobs doesn't whitelist it), or a .coopignore in the root or an ancestor directory matches it (AllowGlobs does NOT override an explicit .coopignore — it's the user's final say). Each directory's .coopignore is loaded once into the closure's cache. ComputeMounts (the mount plan) and `coop check-secrets` (the scanner) share this single rule so "what the box can see" can never drift between them — scanning a path the box hides is pointless, and a secret that IS shadowed is already protected.

func ProfileAuthed

func ProfileAuthed(cfg *config.Config, agent, profile string) bool

ProfileAuthed reports whether the named agent profile has its credential marker file, or is the one default profile represented by a provider-wide env token. Like AuthedAgents, it's a presence heuristic, not a validity check.

func ProfileCredentialReady

func ProfileCredentialReady(cfg *config.Config, agent, profile string, now time.Time) bool

ProfileCredentialReady refines credential presence only where a caller needs a runnable account. Inspectable native markers that cannot recover without another login are excluded; opaque stores and env-backed credentials preserve the presence-based behavior because the adapter cannot prove them invalid locally.

func ProfileMarkerPresent

func ProfileMarkerPresent(cfg *config.Config, agent, profile string) bool

ProfileMarkerPresent reports whether this exact profile has the adapter's login marker. It lets callers distinguish a file-backed account from an env-backed default even after Box has created the profile directory for mounts and session state.

func ProfileTokenMtime

func ProfileTokenMtime(cfg *config.Config, agent, profile string) (time.Time, bool)

ProfileTokenMtime returns when agent's named-profile token material last changed on disk — the mtime of its AuthMarker file (claude's .credentials.json, codex/grok's auth.json). ANY rewrite bumps it: a fresh login OR an OAuth refresh, both of which mint new material and retire the old copy — so this answers "how stale is the token a leak could still use", the signal behind rotating a credential to contain a blast radius. It stats ONLY the marker file, not the whole profile dir, so unrelated session-transcript writes don't masquerade as a rotation. ok=false when the login is an env-key one (no file) or the marker is missing/unreadable — the caller renders that as a graceful "—", never an error.

func ReapOrphanBoxes

func ReapOrphanBoxes(ctx context.Context, rt runtime.Runtime, workspace string) (int, error)

ReapOrphanBoxes removes exactly the boxes SurveyOrphanBoxes finds orphaned for workspace, and reports how many the runtime removed.

It removes by the orphan's own LabelHost value — the same exact-label reap `coop fork stop` uses — so the removal can only ever reach containers that carry the dead supervisor's identity AND this workspace's scope, even if the runtime's view changed since the survey.

func ReapOrphanTempEntries

func ReapOrphanTempEntries(
	ctx context.Context,
	lister mountLister,
	dir string,
	now time.Time,
) (int, error)

ReapOrphanTempEntries removes coop temp files and directories that no box is using.

These leak because cleanup is a deferred call, and a deferred call does not run when the process is killed — which is the normal way a box ends under supervision, a restart, or a machine going to sleep. The happy path already cleans up after itself; this is for every other path.

It refuses to act rather than guess. If the runtime cannot be asked what is mounted, nothing is deleted: a transient failure to list containers must not read as permission to delete the files they are using.

func RenderMounts

func RenderMounts(mounts []Mount, decoyFile, decoyDir string) []string

RenderMounts turns a mount plan into container-runtime arguments. decoyFile and decoyDir are the shared empty read-only file and directory used to shadow secret files and dirs.

func ResolveRepo

func ResolveRepo(override string) (string, error)

ResolveRepo returns the repo root to operate on: the override if set, else the git top-level of the working directory, else the working directory itself.

func Run

func Run(cfg *config.Config, rt runtime.Runtime, spec RunSpec) (int, error)

Run assembles and executes one container run, shadowing secrets and wiring up agent homes + MCP. It returns the container's exit code (with a nil error when the container merely exited non-zero); a non-nil error means it never started.

func ServicesProject

func ServicesProject(repo string) string

ServicesProject is the deterministic per-REPO name: a lowercased, sanitized basename. It is the per-project IMAGE tag — legitimately shared across clones of the same repo (same Dockerfile → same image), so it stays basename-only.

func ShadowCount

func ShadowCount(mounts []Mount) int

ShadowCount is the number of secret paths shadowed (everything but the bind).

func StaleImageInputs

func StaleImageInputs(cfg *config.Config, repo, img string) bool

StaleImageInputs reports whether repo's per-project image was built from a different box Dockerfile/.tool-versions than are on disk now. Best-effort: no box Dockerfile, or no recorded stamp (never built by this coop), returns false — never nag on a guess.

func StalenessNudges

func StalenessNudges(cfg *config.Config, repo, img string) []string

StalenessNudges collects the launch-time staleness warnings for repo's image: per-project input drift, base binary/image skew, and plain old age. Each is one line, best-effort, and never blocks a run; the caller decides where to print them (box.Run for interactive runs, the loop's startup for batch iterations).

func StampImageInputs

func StampImageInputs(cfg *config.Config, repo, img string)

StampImageInputs records the inputs hash for a freshly built per-project image, so a later run can detect drift. A no-op for the shared base (no per-repo inputs).

func StampImageMeta

func StampImageMeta(cfg *config.Config, img, version string)

StampImageMeta records which coop version built the shared base image and the hash of the definition it built from. Called on a successful base build; best-effort.

func StopSessionServices

func StopSessionServices(ctx context.Context, rt runtime.Runtime, workspace, policyRepo string) error

StopSessionServices removes only the current workspace's Compose containers while preserving its volumes. It uses immutable runtime ownership labels instead of the workspace's mutable Compose file, so interrupted agent edits cannot prevent cleanup. The next turn starts services again through EnsureServices.

func ValidateComposeFile

func ValidateComposeFile(path, repoRoot string) error

ValidateComposeFile reports whether the sibling-services compose file at path declares ONLY directives that are safe to auto-run on the HOST daemon — nil when safe, else an error naming the first offending key/path/value. coop runs this before every `compose up` (EnsureServices), so the compose path no longer has to be shadowed read-only in the box: an in-box agent MAY author the file, but the host refuses to run anything that reaches outside a repo-scoped, loopback-only container. repoRoot bounds bind mounts; path's own dir anchors relative binds.

The allowlist is the STRUCT SHAPE: composeDoc/serviceSpec model exactly the safe subset, and the decoder runs with KnownFields(true), so every host-privilege / host-reaching directive (privileged, cap_add, devices, security_opt, userns_mode, pid/ipc/network_mode, env_file, secrets, configs, build, extends, include, a volume's driver_opts, …) is rejected because it is simply absent from the structs — a deny-by-construction that also covers directives compose hasn't invented yet. Only three value checks remain for the fields we DO allow: bind sources must stay within the repo (symlinks resolved), published ports must bind loopback only, and neither may carry a `$` (the file is validated PRE-interpolation, so `${HOME}/.ssh` would read in-repo here yet escape once compose expands it).

func Workdir

func Workdir(cfg *config.Config, repo string) string

Workdir reports where a repo mounts inside the box for a normal run (the agent's cwd): the COOP_WORKDIR override if set, else the repo's own host path. It is the single source of truth for that decision so callers outside box — the loop's stream decoder, which shows tool-call paths relative to this root — stay in step with the real mount. The doctor's spec.Workdir fixture override isn't a normal-run concern, so it's not reflected.

Types

type CompanionRepository

type CompanionRepository struct {
	Name       string `json:"name"`
	HostPath   string `json:"-"`
	BaseCommit string `json:"base_commit"`
}

type Mount

type Mount struct {
	Kind   MountKind
	Source string // host path (Bind only)
	Target string // path inside the box
	RO     bool   // read-only bind
}

Mount is one entry in the container's filesystem plan.

func ComputeMounts

func ComputeMounts(repo, workdir string) ([]Mount, error)

ComputeMounts is the security core: it returns the mounts that bind the repo into the box at workdir and shadow every secret path beneath it. The first mount is always the repo bind; each later mount shadows a secret (DirDecoy for a directory, Decoy for a file). Secret directories are not descended into, so a shadowed dir hides all of its contents at once. The repo's .git is skipped.

A path is shadowed when its basename matches SecretGlobs (unless AllowGlobs whitelists it — templates and public CA bundles stay visible by default), OR a .coopignore — in the repo root or any ancestor directory of the path — matches it (its basename patterns apply anywhere in that directory's subtree; its path patterns are relative to that directory). An explicit .coopignore match is authoritative: it re-hides even an AllowGlobs-whitelisted name.

Its only input is the repo tree plus the .coopignore files in it (no container runtime, no temp files), so it can be exhaustively unit-tested — this is the function that must never let a secret leak.

type MountKind

type MountKind int

MountKind distinguishes the three ways a path enters (or is blocked from) the box.

const (
	// Bind binds a host path to a box path (the repo at the workdir).
	Bind MountKind = iota
	// DirDecoy overlays an empty read-only directory, shadowing a secret directory.
	DirDecoy
	// Decoy overlays an empty read-only file, shadowing a secret file.
	Decoy
)

type OrphanBox

type OrphanBox struct {
	ID       string // container id
	PID      int    // the supervisor it recorded
	Evidence string // its LabelHost value — what the finding rests on, printed by `coop doctor`
}

OrphanBox is one coop box whose supervising host process is provably gone: the pid it recorded is dead, or that pid now belongs to a different process than the one that launched the box.

type OrphanSurvey

type OrphanSurvey struct {
	Checked      int         // every coop box seen, running or stopped
	Orphans      []OrphanBox // in THIS workspace's scope AND provably dead: the only reapable set
	Unattributed []string    // ids coop cannot attribute (no supervisor label, or one it can't read)
}

OrphanSurvey is one scan of the coop boxes a runtime can see, classified for a single workspace.

func SurveyOrphanBoxes

func SurveyOrphanBoxes(ctx context.Context, rt runtime.Runtime, workspace string) (OrphanSurvey, error)

SurveyOrphanBoxes classifies every coop box for workspace without touching any of them.

The judgment is the fork lifecycle's (internal/forkspace.OwnerProvablyDead), applied to an identity carried by the container instead of a pidfile: a box is an orphan only when its recorded supervisor's pid is gone, or that pid has been reused by a different process. Everything else is left alone — a live supervisor, an identity the kernel won't confirm, a box from another workspace, and a box launched before this label existed. Never age, never image, never name.

A query failure is an error, never an empty survey: a runtime that cannot be asked what is running must not read as "nothing is running" to a caller that removes what it doesn't see.

type RunSpec

type RunSpec struct {
	Image   string
	Repo    string   // host repo to mount
	Workdir string   // where Repo mounts; empty defers to resolveWorkdir (the repo's real host path)
	Cmd     []string // command + args to run in the box
	// PolicyRepo is the trusted source for .agent/project.yaml box policy. Empty uses Repo.
	PolicyRepo string
	// RepoReadOnly mounts Repo read-only. Maintenance checks can inspect an isolated candidate
	// without letting the command alter even that disposable tree.
	RepoReadOnly bool
	// RepoReadOnlyPaths remounts real descendant directories read-only after a writable Repo bind.
	// Review stages use it for task queues so source-fixing access never grants lifecycle access.
	RepoReadOnlyPaths []string
	// Review selects the trusted review-only compose file and literal environment. The disposable
	// candidate remains writable for ignored build output; callers verify source identity afterward.
	Review bool
	// CompanionRepositories are policy-pinned snapshots mounted read-only at
	// /coop/repositories/<name>. The remote request surface cannot populate this field.
	CompanionRepositories []CompanionRepository

	Homes   bool // mount per-agent home dirs, env-file, INSTRUCTIONS, and MCP configs
	Network bool // join the sibling-services network if `coop up` created one
	Cache   bool // mount the shared dependency cache volume

	// Agent names the launched registered agent whose credential home and
	// env-file API key this run may mount — so a plain `coop claude` box can't read the
	// other providers' credentials. Empty for a raw/maintenance run (no agent session), which
	// mounts no agent credentials at all. FusionGovernor/ConsultLead (below) widen the
	// scope to the EXPLICIT peers in Peers, since the lead is told to invoke them. See
	// credentialScope. Ignored when Homes is false.
	Agent string

	ForceNoTTY   bool   // ACP: attach stdin (-i) but never allocate a tty
	Serve        bool   // publish .agent/project.yaml serve.ports so a dev server in the box is reachable from the host
	SupervisorID string // non-empty for a supervised inner box: tags it coop.supervised=1
	// (build/update restart it) + coop.sup=<id> (its supervisor kills exactly its boxes)
	ShareACPSessions bool   // mount credential-independent ACP transcript dirs across account switches
	ForkName         string // non-empty for a detached fork loop's box: readable runtime label
	ForkOwner        string // repo-scoped label used by `coop fork stop`; required with ForkName
	RunID            string // the loop run's id; when set, injected as COOP_RUN_ID so a consult peer can append its usage to .agent/runs/<id>.peers.jsonl
	Batch            bool   // loop/doctor: no tty, stdin from /dev/null
	// SuperviseDescendants keeps coop-entry alive after a successful provider exit long enough to
	// drain agent-owned background jobs. It is intentionally opt-in: an interactive box retains
	// the ordinary exec contract and never waits for a shell job the user started.
	SuperviseDescendants bool
	Quiet                bool      // suppress the "shadowed N secret path(s)" line (doctor)
	Stdout               io.Writer // capture output (doctor); nil means inherit os.Stdout
	Stderr               io.Writer // capture/discard the container's stderr; nil means inherit os.Stderr
	ExtraArgs            []string  // extra runtime args for this run (e.g. doctor's probe mount)

	// Ctx, when non-nil, makes the run cancelable: the container runs in its own process group
	// and canceling Ctx tears it down (SIGTERM→SIGKILL). The loop sets this so a second Ctrl-C
	// stops the current iteration now; every other caller leaves it nil — the plain, today's run.
	Ctx context.Context

	// OnRuntimeLaunch, when set, is called exactly once at the runtime-launch boundary: after
	// every host-side step (filesystem projection, sibling services, network inspection, argument
	// assembly) and immediately before the container starts. The loop arms its provider-attempt
	// watchdog here, so a slow host setup is never charged to the provider as silence — and a
	// deadline can only cancel work Ctx can actually reach. It must return promptly: the launch
	// waits on it. A nil hook is the ordinary run, signaling nothing.
	OnRuntimeLaunch func()

	// FusionGovernor, when set, marks this run as fusion mode: the named agent
	// governs (fronts the session) and gets the fusion instruction merged into its
	// instruction file; its peers are consulted read-only. Empty = not fusion.
	FusionGovernor string

	// FusionMembers is the ordered, already-resolved set of coop-consult invocation labels for
	// a Fusion run. Explicit peers use provider names; preset members use role names. The CLI
	// resolves this alongside Peers so box assembly never reconstructs a different council.
	FusionMembers []string

	// ConsultLead names the lead agent of a normal (non-fusion) run: it gets a
	// light, optional "second opinion" directive merged into its instruction file,
	// naming the EXPLICIT peers (Peers) it may consult read-only on hard calls. Scoped
	// to the lead so peers it spawns don't recurse. Empty = no consult directive.
	ConsultLead string

	// AssignedTask is the loop task this iteration owns. The box's prepare-commit-msg hook stamps
	// its Coop-Task trailer, so an agent that forgets one does not lose the whole completion.
	// Empty outside a loop work iteration — nothing is assigned, so nothing is stamped.
	AssignedTask string

	// Peers is the EXPLICIT peer set for this run — the targets named by repeatable
	// --peer (fusion, a normal run, or a loop run), each provider[:model] (no
	// account: a peer runs on its default). It REPLACES the old implicit "every authed
	// agent is a peer" policy: only these providers' credentials mount as peers, only
	// they are named in the consult directive, and the in-box coop-consult refuses any
	// other (COOP_PEERS). A preset's own consult/delegate roles join separately (their
	// role agents also mount + become consultable). Empty = the lead consults no ad-hoc peer.
	Peers []agents.Target

	// Preset, when set, is the loaded orchestration preset for this run: the lead's
	// instruction file gets the generated routing block (roles, modes, exact consult/
	// delegate invocations) instead of the generic consult directive, consult/delegate
	// role agents join the credential scope, and a delegate role mounts coop-delegate
	// plus its per-role contracts and env. The cli loads and applies the preset's
	// model/credential selections before calling Run.
	Preset *preset.Preset
}

RunSpec describes a single container run.

type SecretFinding

type SecretFinding struct {
	Line int    // 1-based line number
	Kind string // what matched, e.g. "OpenAI API key"
}

SecretFinding is one likely secret found in a file's content.

func ScanSecrets

func ScanSecrets(content string) []SecretFinding

ScanSecrets reports likely secrets in content: the provider patterns on every line, plus a conservative entropy check (a long, high-entropy value assigned to a secret-named key). It is pure; callers skip binary/oversized blobs before calling.

type ServicePort

type ServicePort struct {
	Service       string
	ContainerPort int
	HostPort      int
	Scheme        string // "http" (default) or the service's `coop.service.scheme` compose label
}

ServicePort is a sidecar service port coop publishes per-workspace: a service's `expose`d container port, mapped to a stable host port (project.HostPort of the workspace's canonical path). The SAME host port feeds the compose publish (on the host), the in-box forwarder, and the COOP_SERVICE_<NAME>_URL env — so one URL, localhost:<HostPort>, works identically both sides.

func ServicePorts

func ServicePorts(rt runtime.Runtime, workspacePath, composeFile string) []ServicePort

ServicePorts returns the per-workspace host-port mapping for a repo's sidecars: it asks the runtime for the resolved compose config and reads each service's `expose` ports. Best-effort — no compose file, no docker, or a parse error yields nothing (sidecars just aren't published). `expose` (not `ports`) is the opt-in marker: it publishes nothing on its own, so coop's override adds the only host mapping (no double-publish).

type UserGlobs

type UserGlobs struct {
	Base []string
	Path []string
}

UserGlobs are the extra shadow patterns parsed from a repo's .coopignore, split by whether they target a basename (no slash, matched at any depth, like SecretGlobs) or a repo-relative path (contains a slash, matched against the path with filepath.Match, so `config/*.yaml` and `config/creds.yaml` work; there is no `**`).

func LoadUserGlobs

func LoadUserGlobs(repo string) UserGlobs

LoadUserGlobs reads <repo>/.coopignore into a UserGlobs. A missing or unreadable file yields no patterns (the defaults still apply) — it never errors, so a typo'd file can't open a hole by aborting the scan.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL