import { type ExecResult, type ExecutionEnv, ExecutionError, type ExecutionEnvExecOptions, FileError, type FileInfo, type Result } from "../harness/types.js";
import { type SecretEnvFinding } from "../../core/secret-env.js";
import type { EngineNotice } from "../../core/engine-notice.js";
import type { SymlinkChain } from "../harness/types.js";
import type { BackgroundHardKillOutcome, BackgroundShellCapability, BackgroundShellId, BackgroundPoll, BackgroundSpawnOptions } from "../../core/background-shell.js";
import { BackgroundShellError } from "../../core/background-shell.js";
import type { SchedulerCapability, ScheduledIntent, SchedulerContext, ScheduledTaskId, ScheduledTaskSummary } from "../../core/scheduler.js";
import { SchedulerError } from "../../core/scheduler.js";
/** Convert user-facing timeout seconds into a positive, timer-safe millisecond delay. */
export declare function resolveExecTimeoutMs(timeoutSeconds: unknown): number | undefined;
/**
 * Anchor a RELATIVE symlink target at its link's own directory, UNFOLDED and with exactly one separator
 * between the two. A directory that IS a root already ends in its own separator (`/`, `C:\`, `C:/`,
 * `\\host\share\`, all of which `dirname` answers that way), and concatenating a second one mints a
 * DOUBLE-separator spelling for a path that has one spelling — `/tmp -> private/tmp` came back as
 * `//private/tmp`, which the judges that walk this chain then match against nothing, because a guarded row
 * and a containment root are both written with single separators. Joining through `path.resolve`/`path.join`
 * would fix the separator and fold `..` with it, which is the one thing this walk may not do.
 *
 * `separator` is the one the caller's `dirname` writes, passed rather than read from the module so the rule
 * is a pure function of its inputs. A backslash counts as a terminator only where it IS a separator: on a
 * POSIX tree a directory whose name ends in one (`/a/b\`) is an ordinary name, and reading it as terminated
 * would rename `/a/b\/t` to `/a/b\t` — the same class of rewrite as folding `..` lexically.
 */
export declare function anchorAtLinkDir(linkDir: string, target: string, separator: string): string;
/** review finding (win32): `where bash.exe` can resolve to `System32\bash.exe` — the
 *  WSL LAUNCHER, not a native shell. Running it drops into a Linux subsystem where Windows cwd/paths
 *  do not exist; silently adopting it is exactly the "wrong shell, false green" failure D1 forbids.
 *  Filter it (and the WindowsApps alias) out of PATH discovery; Git-Bash candidates are tried first. */
export declare function isWslBashLauncher(p: string): boolean;
/** Shell discovery for the platform (ask ①: exported as the single SDK source so a host
 *  adapter does not mirror the platform logic). Explicit `customShellPath` wins (missing → err);
 *  win32 = Git-Bash discovery (ProgramFiles → PATH, WSL launcher filtered) with fail-loud
 *  `shell_unavailable` (never a silent cmd.exe fallback); POSIX = /bin/bash → PATH bash → sh. */
export declare function getShellConfig(customShellPath?: string): Promise<Result<{
    shell: string;
    args: string[];
}, ExecutionError>>;
/** test seam (mirrors the notice-seat announce reset): the once-per-process fallback latches. */
export declare function __resetSecretScrubAnnouncement(): void;
/** RB-235: the spool-reclaim decision, extracted PURE so every rule is unit-pinnable.
 *  Both reclaim sites (foreground `reclaimSpool`, background `syncSpool`) route through this one
 *  function so they cannot drift apart.
 *
 *  The hazard: `truncateSync(path, 0)` while the writer is LIVE races its O_APPEND appends — a
 *  buffered write landing between the caller's stat and the truncate is destroyed whole
 *  (stdio-block granularity) and NO reader-side counter ever sees it (`truncated` /
 *  `bytesDroppedBeforeCursor` / `bytesFromStart` all undercount by exactly the raced amount). An
 *  earlier narrowing kept the truncate but moved it to the HARD cap, so live truncations became rare
 *  instead of routine; an external black-box audit of that build still measured whole 8192-byte
 *  blocks disappearing, so rarity was never the fix. Ruled 2026-08-03: drop truncate-under-a-live-
 *  writer entirely.
 *
 *  The rule, in one line: **the file of a writer that can still append is append-only.** While the
 *  writer lives, only the reader's cursor moves; physical reclamation happens after it is terminal.
 *   - writer LIVE                                  → never reclaim (no window exists to lose bytes in);
 *   - writer TERMINAL + fully consumed             → truncate at the ROTATE mark (race-free);
 *   - writer TERMINAL + reader behind at HARD cap  → truncate, byte-accounting the unread remainder
 *     (`skippedBytes` → recordSkippedBytes, the honest `[... N bytes truncated ...]` disclosure).
 *
 *  Cost, stated honestly: a live producer that outruns the reader is no longer capped on disk — its
 *  spool grows for as long as it runs (bounded by the exec timeout / background hard wall, reclaimed
 *  in full at its death). Capping it would mean destroying bytes we cannot count, which is the defect
 *  itself; no portable Node syscall releases the consumed prefix of a file another process is
 *  appending to. The reader stays bounded regardless — `skipAhead` drops the un-retainable middle and
 *  accounts every skipped byte. The same unbounded-while-live posture already holds for the retain
 *  lane (design/128) and for the detached-descendant residual (RB-26), and matches the CC anchor's
 *  shape (named temp files, no rotation at all) with the file never outliving its writer.
 *
 *  RESIDUAL, unchanged by this rule: `writerLive` describes the DIRECT child. A detached descendant
 *  holding the same fd can still append after the child is reaped, so a terminal-writer truncate can
 *  in principle race it — the documented RB-26 posture, and undetectable from this side of the fd. */
export declare function decideSpoolReclaim(args: {
    size: number;
    cursor: number;
    writerLive: boolean;
    rotateBytes?: number;
    hardCapBytes?: number;
}): {
    reclaim: false;
} | {
    reclaim: true;
    skippedBytes: number;
};
/** design/128 T1-1: open the spool file pair ALL-OR-NOTHING — a failure opening the
 *  second file must close the first fd before rethrowing (the caller's catch only clears its references, so
 *  a half-open pair would leak fdOut for the env's lifetime). `wx` + 0600 (never follow / never reuse an
 *  existing path). Exported for direct fault-injection tests (pre-create a path → EEXIST). `a` (not
 *  `w`): O_APPEND makes every child/descendant write an atomic append at the current end — which is
 *  what lets the foreground quota rotation truncate a fully-consumed file to zero and have the next
 *  write land at offset 0 (a plain-fd writer would keep its own offset and leave an unreadable hole). */
export declare function openSpoolPair(base: string): {
    outPath: string;
    errPath: string;
    fds: [number, number];
};
export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCapability, SchedulerCapability {
    cwd: string;
    /** `ExecutionEnv.homeDir` = the `HOME` the shell child receives (its `~`/`$HOME`/`${HOME}` all expand to it), read LIVE off the same `shellEnv`-over-`inheritEnv` merge each exec gets; ABSENT when the
     *  child gets none (an allowlist that withholds it: `~` then falls back to passwd while `$HOME` is empty — two values, no single home, home-relative operands stay fail-closed). Residual: a per-exec `env.HOME` override. */
    get homeDir(): string | undefined;
    private shellPath?;
    private shellEnv?;
    private inheritEnv;
    private readonly scheduler?;
    private readonly bgShells;
    private bgCounter;
    private readonly retainBackgroundProcesses;
    private readonly onSecretEnvScrub?;
    private readonly onNotice?;
    /** design/128 T1-1: lazily-created env-owned spool dir (mkdtemp under os tmp — unpredictable, never the cwd). */
    private bgSpoolDir?;
    readonly backgroundCapabilities: BackgroundShellCapability["backgroundCapabilities"];
    constructor(options: {
        cwd: string;
        shellPath?: string;
        shellEnv?: NodeJS.ProcessEnv;
        /** VENDORED EDIT (D1, security): how much of `process.env` the shell child inherits. Default `"scrub"` =
         *  fail-closed (drop secret-pattern keys, keep PATH/build/locale). `"all"` = full inherit (trusted env);
         *  `string[]` = strict allowlist. `shellEnv`/per-exec `env` always pass through. Any other value — an unknown
         *  word, a non-string, a list entry that is not a variable name — throws here (`inheritEnvPolicyProblem`'s
         *  reason), the same judgment the oracle gate's `inheritEnv` meets. */
        inheritEnv?: "all" | "scrub" | string[];
        /** design/105: inject a resident scheduler daemon backend (the shell daemon). Absent ⇒ scheduling INERT. */
        scheduler?: SchedulerCapability;
        /**
         * design/128 T1-1 — deliver-a-living-service mode (default false). When true, background processes
         * OUTLIVE the run: `disposeBackgroundShells()` becomes a no-op (the runner's every-exit-path dispose
         * included) and background stdio is spooled to env-owned temp files (0600, `wx`, unpredictable path)
         * so the child never dies of EPIPE after the host exits. DEPLOYMENT-declared at env construction (the
         * env lane; `TaskSpec.retainBackgroundProcesses` is the task lane, without this spooling guarantee on
         * every platform — docs/background-process-lifetime.md). The 2h bg-timeout reaper and `killBackground` still work
         * while the host lives; after host exit, cleanup is the deployment's responsibility (e.g. the TB
         * container teardown). v1 covers processes spawned via `execBackground` only (a detach-adopted
         * foreground exec keeps pipe stdio — best-effort survival). Incompatible with durable-suspend
         * deployments (their pre-suspend dispose becomes a no-op too — do not combine).
         */
        retainBackgroundProcesses?: boolean;
        /** The D1 scrub decision's observation seat: called with every NON-EMPTY finding set the default
         *  "scrub" leg withholds from a shell child. Absent ⇒ a once-per-process console summary (loud
         *  enough to notice, quiet enough not to spam). Never called on the "all"/allowlist legs. */
        onSecretEnvScrub?: (findings: readonly SecretEnvFinding[]) => void;
        /** #1081 — the structured-notice seat for the SAME withheld-keys disclosure (`EngineNotice`,
         *  `code: "config.secret_env_scrubbed"`, `detail.names`); independent of `onSecretEnvScrub` (a raw
         *  findings collector) except: that seat wired alone stays console-silent, as before this existed. */
        onNotice?: (notice: EngineNotice) => void;
    });
    get schedulerCapabilities(): SchedulerCapability["schedulerCapabilities"];
    schedule(intent: ScheduledIntent, ctx: SchedulerContext): Promise<Result<{
        id: ScheduledTaskId;
    }, SchedulerError>>;
    cancel(id: ScheduledTaskId, ctx: SchedulerContext): Promise<Result<void, SchedulerError>>;
    list(ctx: SchedulerContext): Promise<Result<ScheduledTaskSummary[], SchedulerError>>;
    absolutePath(path: string): Promise<Result<string, FileError>>;
    joinPath(parts: string[]): Promise<Result<string, FileError>>;
    exec(command: string, options?: ExecutionEnvExecOptions): Promise<Result<ExecResult, ExecutionError>>;
    readTextFile(path: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
    readTextLines(path: string, options?: {
        maxLines?: number;
        abortSignal?: AbortSignal;
    }): Promise<Result<string[], FileError>>;
    readBinaryFile(path: string, abortSignal?: AbortSignal): Promise<Result<Uint8Array, FileError>>;
    /** RB-221: whole-file replace via same-directory staging + fsync + rename (CC 2.1.220 `Q5`,
     *  (pretty220.js:60808, historical; not relocated in 250 — see cc-250 anchors)). The former single-step `writeFile` was an in-place O_TRUNC: an abort mid-write
     *  (Esc during a large Write — ctx.signal reaches here directly), ENOSPC, or a crash left the target as
     *  a torn prefix of the NEW content with the original unrecoverable. Post-fix contract: the target is
     *  either fully old or fully new; `aborted` means it was never touched. Known semantic change (same as
     *  CC): rename breaks a hard link — the other link keeps the old content. */
    writeFile(path: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
    /** RB-221: the historical single-step in-place overwrite — kept for targets where
     *  staging+rename would change node identity (symlink chains, non-regular files) and as the
     *  CC-parity fallback for filesystems where the rename cannot work. NOT atomic by design. */
    private writeFileInPlace;
    /** Atomic exclusive create ({@link FileSystem.writeFileExclusive}): Node `wx` flag = one
     *  `open(O_CREAT|O_EXCL)` syscall — an existing file returns FileError `"already_exists"` and is left
     *  untouched. The EEXIST→already_exists mapping lives HERE, on the exclusive-open step only — a parent
     *  `mkdir` can also throw EEXIST (parent path component is a regular file) and that is NOT "the target
     *  already exists" (fable review). Parent directories are created like writeFile. */
    writeFileExclusive(path: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
    appendFile(path: string, content: string | Uint8Array): Promise<Result<void, FileError>>;
    fileInfo(path: string): Promise<Result<FileInfo, FileError>>;
    /**
     * design/381 DV-8 — {@link FileInfo.mode}'s write half. NO-FOLLOW by construction: `lchmod` exists
     * only on BSD, so a symlink at the leaf is REFUSED (`invalid`) instead of being traversed, which
     * would re-permission whatever the link points at — the same posture every other guard in this
     * family takes when it cannot verify what it is about to touch. Absent on platforms without the
     * permission face ({@link HAS_POSIX_MODE_FACE}), where the caller's disclosure is the honest answer.
     */
    setFileMode(path: string, mode: number, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
    listDir(path: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
    canonicalPath(path: string): Promise<Result<string, FileError>>;
    /** {@link FileSystem.fileIdentity}: `stat` (links followed) read as BIGINT — an inode past 2^53 (an APFS volume root)
     *  is not exact as a number, and two inodes that collapse to one number would name two directories as one. */
    fileIdentity(path: string): Promise<Result<string, FileError>>;
    readLink(path: string): Promise<Result<string, FileError>>;
    /**
     * The LEAF symlink chain ({@link FileSystem.canonicalChain}): `readlink` on the whole spelling, then on each
     * target, until a hop is not a link (`EINVAL`), is absent (`ENOENT` / `ENOTDIR`), repeats, or the bound is
     * reached. A relative target is ANCHORED at its link's directory and never folded — `..` is the filesystem's
     * to resolve during the next `readlink`'s own lookup (a lexical fold reads `bridge/../next` as `next` and misses
     * every link behind `bridge`). Anchoring puts EXACTLY ONE separator between the directory and the target
     * ({@link anchorAtLinkDir}); a root directory already ends in its own. One syscall per hop, no `lstat`. On a
     * win32 host a share-shaped hop is kept and ends the walk unprobed. Any other errno is an answer nobody can
     * use (`err`), never a shortened chain.
     */
    canonicalChain(path: string): Promise<Result<SymlinkChain, FileError>>;
    exists(path: string): Promise<Result<boolean, FileError>>;
    createDir(path: string, options?: {
        recursive?: boolean;
    }): Promise<Result<void, FileError>>;
    remove(path: string, options?: {
        recursive?: boolean;
        force?: boolean;
    }): Promise<Result<void, FileError>>;
    createTempDir(prefix?: string): Promise<Result<string, FileError>>;
    createTempFile(options?: {
        prefix?: string;
        suffix?: string;
    }): Promise<Result<string, FileError>>;
    /** design/116 detach: adopt a RUNNING foreground child as a background shell. Pre-fills the bg tails with
     *  the output captured so far (so TaskOutput's first read has the full history), attaches fresh data/close
     *  handlers, and arms the default bg timeout. Returns undefined when the live-shell limit is hit (detach
     *  refused; the exec keeps running in the foreground). */
    private adoptRunningChild;
    spawnBackground(command: string, options?: BackgroundSpawnOptions): Promise<Result<{
        shellId: BackgroundShellId;
    }, BackgroundShellError>>;
    pollBackground(shellId: BackgroundShellId): Promise<Result<BackgroundPoll, BackgroundShellError>>;
    killBackground(shellId: BackgroundShellId): Promise<Result<void, BackgroundShellError>>;
    /** The synchronous hard kill seat ({@link BackgroundShellCapability.killBackgroundNow}). Reads process truth,
     *  never `entry.status` (other stops flip it before their attempt, which can fail or wait on a timer). Once
     *  node has observed the leader's exit its pid may name a stranger: `already_gone`, nothing signalled. Before
     *  that the leader is still our child (live or an unreaped zombie), so ONE `kill(-pid, SIGKILL)` reaches its
     *  group and nothing else — no descendant snapshot (a process that left the group is outside this seat).
     *  win32 has no synchronous hard kill: no process groups, and the tree kill's MSYS group discovery needs its
     *  deferred step, so this seat answers `no_pgid` and the caller's ordinary kill (the ladder) does the work.
     *  Only a landed signal sets the latch; a refused one is `no_pgid`. */
    killBackgroundNow(shellId: BackgroundShellId): BackgroundHardKillOutcome;
    /** design/128 T1-1: incrementally read a retained shell's spool files into the in-memory tails, so
     *  pollBackground serves the same cursor semantics whether stdio came over pipes or spool files.
     *  Bounded single read per stream per poll (the tail keeps only its rolling window anyway).
     *  reads are positioned by spool.fileCursor (the FILE offset — rotation truncates
     *  the file), while poll slicing keeps using the logical stream totalBytes; the two advance in
     *  lockstep here but are NOT interchangeable after a rotation. */
    private syncSpool;
    /** +: reclaim an EPHEMERAL spool (detach-adopted, or any non-retain bg
     *  spawn — the whole unix bg lane is spool-backed now) — fold the final residue into the tails,
     *  unlink both files, and drop the spool reference (later syncs become no-ops). Idempotent;
     *  never touches design/128 retain-lane spools (no `ephemeral` flag). A still-writing detached
     *  descendant keeps its fd into the unlinked inode (RB-26 posture — no EPIPE, auto-reclaim at
     *  its exit). */
    private reclaimEphemeralSpool;
    disposeBackgroundShells(opts?: {
        except?: readonly BackgroundShellId[];
    }): Promise<void>;
    cleanup(): Promise<void>;
}
