/** * Credential helper script renderer — generates a POSIX `sh` script that git * invokes via `credential..helper = !sh ` to fetch credentials * for a Tier-2 git mount. * * The script implements the wake-mid-401 hardening described in * `_devlog/specs/2026-05-07-unified-credential-mediation.md` § Step 9a: * * 1. On `op = get`, stat the credentials file and compute its age (mtime). * 2. If the file is empty/missing OR age > triggerAgeSec, touch the * refresh-flag file. The runner watches this path via `fs.watch` and mints * a fresh access token + rewrites the credentials file when it sees the * change. Emptiness is its own trigger because a freshly truncated file * has a *recent* mtime, so the age check alone can never heal it. * 3. Poll the credentials file in 200ms increments until its mtime advances, * or it becomes non-empty (when it started empty), or waitTimeoutMs * elapses. * 4. Translate the credentials file (`scheme://user:pass@host ` lines — * git's `store --file=` format) into the helper-output format git * expects (`username=\npassword=\n`). Filters lines by the * mount's url-prefix tag so multi-mount workspaces stay isolated. * * Two properties beyond the original Step 9a design: * * - **The dance is bounded.** A refresh that keeps failing would otherwise * make every single git network op pay a fresh `waitTimeoutMs` stall * forever. The script stamps `refreshAttemptPath` after a dance that * changed nothing and refuses to dance again within `refreshCooldownSec`. * - **No credential is reported, not hidden.** Exiting 0 with no output makes * git ask the next helper and then the terminal, so the operator sees * `fatal: could not read Username for 'https://github.com'` — an error that * names neither the mount nor the real cause. The script instead writes an * attributable `skaile:` line to stderr and emits `quit=1`, which is the * one mechanism that actually stops git's helper chain (a non-zero exit * does not: git falls through to the username prompt anyway). * * When `unavailableReason` is set the mount is known not to be connected, and * the script skips the refresh dance entirely: the runner's refresh dispatcher * only iterates *connected* git connectors, so nothing on the runner side could * service the request — firing one would be a guaranteed-useless stall. * * **Cross-platform**: targets POSIX `sh` (Linux + macOS). Avoids bashisms so * `dash`, `ash`, and `busybox sh` all work. The two GNU/BSD divergence points * are handled inline: * * - `stat -c %Y ` (GNU/Linux) vs `stat -f %m ` (BSD/macOS) — * try GNU first, fall back to BSD, default to 0. * - `sleep 0.2` is non-portable — use `usleep 200000` when present, else * fall back to `sleep 1`. * * The credentials file format is unchanged (Option B in the Step 9a plan): * `scheme://user:pass@host ` per mount, written by `writeMountBlock`. * The helper script does the translation to `username=<>\npassword=<>\n` * inline so we don't have to change the on-disk format and break Step 1A * back-compat. * * Spec: `_devlog/specs/2026-05-07-unified-credential-mediation.md`. */ /** Required arguments for {@link renderCredentialHelperScript}. */ export interface RenderHelperOpts { /** Absolute path to the per-mount credentials file. */ credentialsPath: string; /** * Absolute path to the workspace-level refresh-request flag file. The * runner watches this file via `fs.watch` and triggers a token refresh on * `change`. */ refreshFlagPath: string; /** * Tag emitted by `writeMountBlock` on the credential line for this mount * (e.g. `# skaile-mount: workspace`). The helper greps for lines ending in * this tag so concurrent mounts in the same credentials file stay isolated. */ credentialTag: string; /** * Touch the refresh-flag if the credentials file is older than this (in * seconds). Default: 300 (5 minutes — comfortably shorter than the typical * 1-hour Anthropic OAuth lifetime, well clear of the 5-minute pre-expiry * refresh window scheduled by the driver). */ triggerAgeSec?: number; /** * Maximum time to wait for the credentials file mtime to advance after * touching the refresh-flag (in milliseconds). Default: 5000 (5 seconds — * long enough for the runner's debounced watcher + token mediator round- * trip; short enough that a stuck backend doesn't block git for minutes). */ waitTimeoutMs?: number; /** * Set when the mount is known **not** to be connected — the connector failed * at connect time, so no credential will ever appear. The script then skips * the refresh dance and immediately tells git to stop, quoting this reason. * * The refresh dispatcher only iterates connected git connectors, so a failed * connector has nothing on the runner side that could service a refresh * request; firing one would stall for `waitTimeoutMs` and change nothing. */ unavailableReason?: string; /** * Minimum gap, in seconds, between two refresh dances for this mount. * Default: 30. Bounds the cost of a refresh that legitimately keeps failing. * * A throttled invocation skips only the *wait*, never the read: the * credential lookup below runs unconditionally, so a token that landed before * the invocation is still served. The window it gives up is narrow — a * mediator round-trip that finishes after `waitTimeoutMs` but before the * cooldown expires — and the trade is deliberate: a bounded failure carrying * an accurate reason beats an unbounded stall on every git operation. */ refreshCooldownSec?: number; /** * Absolute path to the per-mount stamp file the script writes itself after a * dance that produced nothing. Defaults to a sibling of the credentials file * derived from the tag; callers should pass it explicitly so the name is * predictable. Only this script reads or writes it — the runner never does. */ refreshAttemptPath?: string; } /** * Render the credential-helper script body. The returned string should be * written to disk with mode `0755` (owner rwx, group/other rx) so git can * execute it. * * `git` invokes credential helpers with one of three operations as `$1`: * - `get` — read credentials. We respond with the helper-output format. * - `store` — git wants us to remember a credential. We're a one-way * read-only helper; ignore. * - `erase` — git wants us to forget a credential. Same — ignore. * * Only `get` triggers the refresh-flag dance. Other ops exit silently with 0 * so git falls back to the next helper in the chain (or its own prompt). * * The script also drains stdin on `get` because git always pipes the request * envelope (host, protocol, path, etc.) into the helper's stdin. Leaving it * unread can wedge git's helper FD. */ export declare function renderCredentialHelperScript(opts: RenderHelperOpts): string; //# sourceMappingURL=credential-helper-script.d.ts.map