/** * Integration + env discovery — the preflight's static pass (design: docs/designs/clustly-cli.md * §5c.1; build-plan slice 2.5). Scans the shippable files for env-var references and outbound * hosts, maps the agent's model provider (including the subscription-CLI edge: a `claude-cli/*` * stack has NO key on disk — hosted runs need the API-equivalent provider key), and merges the * results ADDITIVELY into clustly.yaml (`env` names + `egress` domains). Builder entries are * never removed; review approves the final egress list. Secret VALUE resolution is slice 3.5. */ import { type Framework } from "./discovery"; import { type ClustlyManifest, type CredentialMode } from "./manifest"; import { type CollectedFiles } from "./scanner"; export interface ProviderMapping { /** The model id as configured locally (e.g. `claude-cli/claude-opus-4-8`). */ model: string; provider: string; /** Env var the hosted runtime needs for this provider. */ envVar: string; domain: string; /** True when the local stack authenticates via a subscription CLI — no importable key exists. */ subscriptionCli: boolean; } export interface Inventory { /** Env var names referenced by the agent's code/config, sorted. */ envVars: string[]; /** Outbound hosts referenced, sorted (localhost/private hosts excluded). */ domains: string[]; /** file → what was found there (the table the builder sees). */ byFile: Record; provider?: ProviderMapping; } /** * Names the SANDBOX RUNTIME supplies — never secrets the builder sets through `clustly secrets`. * * The env patterns above match every `process.env.X` read, and a normal agent reads plenty that * are not credentials: `HOME` to find a config dir, `PORT` to bind, `NODE_ENV` to pick a mode. * Listing those under `env:` told the builder their agent "needs" a secret nobody can sensibly * set, and the deploy then refused to serve pending it (external builder report, 2026-08-22). * Exact-match only: `PATH` is the runtime's, `PATH_TO_MODEL` is the builder's. */ export declare const RUNTIME_ENV_NAMES: readonly ["HOME", "PATH", "PWD", "TMPDIR", "TMP", "TEMP", "NODE_ENV", "PORT", "HOSTNAME", "USER", "LOGNAME", "SHELL", "TERM", "LANG", "LC_ALL", "TZ", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY"]; export declare function mapModelProvider(model: string): ProviderMapping | undefined; /** * The env names this provider needs under a given credential mode. * * Credential modes apply exactly when the stack runs through Claude Code — which is what * `subscriptionCli` already marks. That is not a proxy for "is Anthropic": the plain `anthropic` * provider talks to the REST API directly and cannot authenticate with a `setup-token` credential * at all, so BYOK is its only option and it keeps its single fixed name. Every non-Claude provider * is likewise untouched by any of this. */ export declare function providerEnvNames(provider: ProviderMapping, mode?: CredentialMode): string[]; /** * The builder's installed OpenClaw engine version — the hosting side hosts on a VETTED pin * and fails closed on anything else (design §6b), so the manifest must carry it. Injectable * exec for tests; undefined when the CLI isn't on PATH (the wizard says what to do). */ export declare function detectOpenclawVersion(exec?: (cmd: string, args: string[]) => string): string | undefined; export declare function detectOpenClawModel(home: string): string | undefined; export declare function scanInventory(workspace: string, collected: CollectedFiles, home: string, /** The detected workspace framework. Gates the local-OpenClaw-config read below — see there. */ framework: Framework): Inventory; export interface MergeResult { manifest: ClustlyManifest; addedEnv: string[]; addedEgress: string[]; /** Credential names dropped because the manifest's mode no longer needs them — never a builder's * own entry. Empty on every deploy that does not change credential mode. */ removedEnv: string[]; /** * Names the manifest declares that neither the source scan nor the credential mode asks for. * The merge is additive and the manifest carries no provenance, so a name the scanner wrote * on an earlier run stays after the code stops reading it — invisible while `--dry-run` keeps * passing, then a SECRETS_MISSING block on the next real deploy, for a secret nothing needs * (bug report 2026-09-17, B5). Reported, never pruned: it may be a hand-written entry a * dependency reads. */ unreferencedEnv: string[]; } /** * Merge the inventory into the manifest and rewrite clustly.yaml only when it changed. * * ADDITIVE, with one deliberate exception. Builder entries are never removed — that rule is what * lets a seller hand-edit `env:`/`egress:` and keep their edits. But the two credential names in * `MANAGED_CREDENTIAL_ENV` are ours, not theirs, and leaving a stale one behind after a mode switch * produces a manifest requiring both credentials at once. So a credential name is dropped when the * mode stops needing it AND nothing in the agent's own source referenced it — the scan is what * protects an agent that genuinely reads `ANTHROPIC_API_KEY` in its own code. */ export declare function mergeInventory(workspace: string, manifest: ClustlyManifest, inventory: Inventory, /** * §4b (2026-08-24): scanner-inferred egress hosts are SUGGESTIONS, not commitments — the * caller passes the subset the builder confirmed. Undefined = the pre-§4b behavior (add * everything), kept so a caller that has no confirmation step is unchanged rather than * silently dropping hosts. The provider's own domain always merges regardless: it comes * from the chosen credential mode, not from source scanning, and hosted runs need it. */ approvedEgress?: string[]): MergeResult;