/** * "Has Windows been told to run this?" — asked of Task Scheduler, by name. * * A RUNTIME question, not a diagnostic one. `elevated-availability.ts` asks it * on every reconcile to decide which word a machine with no reachable helper * publishes to the relay (`not_registered` vs `endpoint_unreachable`), and the * doctor asks the same question when it prints a report. It lives here, on its * own, because the dependency may only run in that direction: fail-closed * runtime logic must not import from the diagnostics subtree, or removing or * moving `doctor/` breaks the agent and the availability rule can no longer be * reasoned about by itself. The tri-state capture primitive it is built on * (capture.ts) was moved out of `doctor/checks/` for exactly that reason. * * THERE IS ONE OF THESE. There were briefly two — the doctor's and the * runtime's — and they diverged on the case that matters most, which is how the * `queried` sentinel came to be computed by one copy and thrown away by the * other. `doctor/checks/windows.ts` re-exports this module; it does not have its * own. * * FULL PATHS, NEVER BARE NAMES. `powershell.exe` is spelled out under * `%SystemRoot%\System32`: this runs from the tray, from a service and from the * SYSTEM-owned helper, and in none of those is `PATH` ours to trust. * * NOTHING READ HERE EVER BECOMES SOMETHING WE RUN. The only value interpolated * into the script is a compile-time constant of ours, quoted as a PowerShell * literal; everything Windows answers is parsed and reported. */ /** `C:\Windows`, or whatever this machine calls it. */ export declare function systemRoot(): string; export declare function powerShellPath(): string; /** * How long the scheduled-task query may take before we give up on it. * * Far above `capture.ts`'s 5 s default, and deliberately: `Get-ScheduledTask` * cold-loads the ScheduledTasks CIM module on first use, and on a box under * behavioural AV scanning — the machine this whole effort is aimed at — that * alone regularly runs past five seconds. A timeout there is not a cheap "we * could not tell": it is the reason the incident machine could never produce * `not_registered`, the one verdict worth producing. Still bounded, because * nothing that can hang may sit on the reconcile path. */ export declare const SCHEDULED_TASK_QUERY_TIMEOUT_MS = 25000; /** What Windows said about a task it DOES know about, or does not. */ interface ScheduledTaskAnswer { /** * Whether the QUESTION was actually answered — i.e. PowerShell ran and told us * something, as opposed to not running at all. * * This is the discriminant of the union, and that is the point: `registered` * exists ONLY on this branch, so a caller cannot obtain "is it registered" * without having first established that the question was answered. Reading a * bare boolean was possible for one release and produced three separate bugs, * all of the same shape — "we could not ask" published as "it was never * registered", which is the 2026-09-02 incident's own signature invented on a * machine that may be perfectly healthy. */ queried: true; /** Windows named the task (`false` = Windows answered and did not name it). */ registered: boolean; state: string | null; /** The action's own program (`powershell.exe`, the tray exe, …). */ execute: string | null; /** The action's argument string, as registered. */ args: string | null; /** * Whether the process that ASKED is a member of Administrators — `null` when * even the identity call was refused. * * It changes what an absent task means. Task Scheduler enforces a per-task * ACL, and the installer registers the Relaunch task with SDDL * `O:BAG:SYD:(A;;FA;;;SY)(A;;FA;;;BA)` — SYSTEM and Administrators only, no * Users grant (desktop/build/win-update-task.ps1, `$relaunchSddl`). A * standard-user tray asking about it is told there is no such task, which is * indistinguishable from its never having been registered. Without this, "the * task is missing" would fire on every packaged install running as a normal * user — the false alarm that costs a diagnostic its credibility. * * It is now the SECOND line of defence, not the first: a denied lookup is * caught below by the COM HRESULT and comes back `queried: false`, never as an * answer. What is left for this field is the case COM cannot discriminate — * `Get-ScheduledTask` returning the task but with an action the caller may not * read, or a future ACL shape neither call resolves — plus the plain reporting * value of "who was asking" beside an absence. It is deliberately NOT justified * by the blocked-COM case: a host that blocks `Schedule.Service` returns the * unanswered branch, which has no `elevated` field at all. */ elevated: boolean | null; } /** * WHICH KIND of "we could not find out" this was. * * It exists because callers were writing ONE remedy for all of them — "re-run * elevated" — which is right for exactly one and misleading for the rest: no * amount of elevation fixes a missing `powershell.exe`, a killed query or a * ConstrainedLanguage host. A caller that wants to say something about the cause * must branch on this rather than on the prose of `reason`. * * - `denied` — Task Scheduler refused to let this account read the task. * The task EXISTS (that is what 0x80070005 means, as opposed * to 0x80070002), and elevation is what would answer it. * - `contradiction` — the two lookups disagree: `Get-ScheduledTask` did not find * the task, the Task Scheduler service read it out of the root * folder. SOMETHING was learned — the service saw it — but two * lookups disagreeing is not a positive answer, so it is not a * registration. It has its own value precisely so a caller does * not have to say "nothing was learned either way", which is the * one thing that is NOT true here. * - `lookup_failed` — the lookup broke. Nothing was learned about the task, * including whether it exists. * - `unavailable` — the query never ran, or ran and printed no answer. */ export type ScheduledTaskUnansweredCause = "denied" | "contradiction" | "lookup_failed" | "unavailable"; /** * THE ONE SENTENCE ABOUT A REFUSAL, exported so there can only be one of it. * * `cause: "denied"` is 0x80070005 raised by `$folder.GetTask()` itself — * the only stage of the COM probe that has SEEN the name (the three-stage note * in the script below is what makes that true) — so a denial proves the named * task EXISTS. Every caller that reports one says so with this clause instead of * wording it again: for one release the two doctor checks described this single * measured fact in opposite words, `persistence.ts` opening `"…" IS registered` * while `priv-helper.ts` opened "whether … is registered could not be * determined" and then asserted existence two clauses later. * * WHAT IT DOES NOT SAY, and no caller may add: what the task points at, or that * it runs. The refusal withheld the DEFINITION — the action, the state, the * path — which is exactly the half a registration check exists to read. That is * why a denial still comes back `queried: false` with no `registered` field: * "exists, definition unreadable" is a weaker fact than "registered", and * promoting it would let an ACL refusal alone stand in for a working helper. */ export declare const DENIAL_PROVES_TASK_EXISTS = "Task Scheduler refused this process the right to read it, which it only does for a task that exists"; /** * No answer came back — the query did not run, broke, or was refused. A statement * about the DIAGNOSTIC, never about whether the task is registered. */ interface ScheduledTaskUnanswered { queried: false; cause: ScheduledTaskUnansweredCause; /** Why, in one line, for the `detail` of a check that reports itself skipped. */ reason: string; } /** * THE ROOT-FOLDER CONTRACT, and it is load-bearing rather than decorative. * * `Get-ScheduledTask -TaskName x` searches EVERY TaskPath; the COM discriminator * below asks `GetFolder('\')` and therefore searches only the root. For a task * registered under, say, `\Microsoft\Windows\…`, the two disagree by * construction: COM would answer 0x80070002 ("not in this folder") and we would * publish a confident absence about a task that is merely somewhere else. * * The contract is that everything this module is asked about is registered at * `\`. MEASURED, not deduced: `Get-ScheduledTask | ? TaskName -like 'AI * Commander*'` reports `TaskPath=\` for all three of ours on three live boxes, * and our installer registers all three there. A recursive folder walk would * remove the assumption but costs a COM round trip per folder inside a 25 s * budget that already loses to a CIM cold-load, so the assumption is kept and * ENFORCED instead: an `ABSENT` for a name outside this set degrades to * `lookup_failed`. * * WHAT THIS SET DOES NOT DO, because it reads like it does: it constrains NAMES, * not LOCATIONS. Membership asserts a measurement about where the installer puts * a task; it cannot make a task be there. If one of these three were ever * registered somewhere other than `\` AND its ACL denied us — so that * `Get-ScheduledTask` throws ObjectNotFound and the root-folder COM call misses * it — this module would publish a confident `registered: false` about a task * that exists. The absence verdict is only as sound as that measurement. Move a * task out of `\` and this needs the folder walk; add a name here only after * checking its TaskPath. */ export declare const ROOT_FOLDER_TASKS: ReadonlySet; /** * Windows' answer, or the absence of one. * * Three states, not two: registered, answered-and-absent, and never-asked. The * third has no `registered` field at all — see `queried`. */ export type ScheduledTaskInfo = ScheduledTaskAnswer | ScheduledTaskUnanswered; /** * Ask Windows about ONE scheduled task by its constant name. * * `queried: true` means Windows answered — INCLUDING the case where the answer * arrived as partial output before the deadline killed the process, but NOT the * case where the process was killed while the lookup was still running. Starting * to ask is not being answered. * * `queried: false` covers BOTH "we could not ask" and "Windows refused to answer" * — the second being the denial that started all this, where the task provably * exists — and no caller may read either as "the task is absent". Which of them * happened is on `cause`; a caller writing a remedy must branch on it rather than * assume the ACL case (see ScheduledTaskUnansweredCause). */ export declare function queryScheduledTask(name: string, opts?: { timeoutMs?: number; }): Promise; export {};