/** * Where configuration comes from, and — more importantly — WHEN it is read. * * == Nothing is required to start the server * * The bootstrap ships two tools with genuinely different needs: the lint tool * shells out to a local binary and needs no SpecGuard deployment at all, while * the repository tool needs both an endpoint and a key. A server that validated * everything at boot would refuse to start for someone who installed it purely * to lint — and MCP's failure mode for that is not a helpful message, it is a * client reporting "server exited" with the reason buried in a log nobody * reads. * * So `loadConfig` never throws. Each tool asks for what it needs, at call time, * through the `require*` helpers below, and a missing variable comes back as one * legible sentence in a tool result the agent can act on. This is also the * property that keeps the toolset growable: a tool added later that needs a * third variable adds a `require*` helper and changes nothing about startup. * * == SPECGUARD_ENDPOINT, with SPECGUARD_URL as an accepted alias * * `SPECGUARD_ENDPOINT` is the name the shipped `specguard-ruby` gem already * reads, so a repository whose CI posts runs to SpecGuard has it set — that is * the whole reason this server borrows the name rather than coining one. The * SPGD-310 brief writes it as `SPECGUARD_URL`, so that spelling is accepted * too rather than left as a silent no-op for anyone who follows the ticket. * `SPECGUARD_ENDPOINT` wins when both are set and disagree, because it is the * one the rest of the toolchain is already reading. * * == THREE KEY VARIABLES, because SpecGuard has three credentials that refuse * == each other * * `Api::BaseController` discriminates on the token's PREFIX *before any table is * read*, and answers 401 on a mismatch without a lookup: `sgk_` names ONE * repository (`GET /api/v1/repository`, `POST /api/v1/ingest`), `sgu_` names a * PERSON (`GET /api/v1/repositories` and its writes), and — SPGD-952 — `sga_` * names AN AGENT: nobody's account and no single repository, but an explicit * set of repositories with a permission set, both fixed at mint time * (`AgentApiKey::TOKEN_PREFIX` says all three prefixes are deliberately the same * length so neither can be a prefix of another — the mutual refusal is designed, * not incidental). * * One variable therefore cannot serve two kinds, let alone three: whichever * kind it holds, the tools needing another kind 401. So there are three, read * independently, and none is required — an operator who only ever calls the * repository tool sets only `SPECGUARD_API_KEY`, exactly as before any of this * existed. Prefix-dispatching over a single variable was the alternative and it * cannot work: an operator wanting several kinds of tool needs several keys * present at once. * * Which variable a tool reads is then carried onto `ApiConfig` alongside the * value — see `Credential` — for the same reason `endpointVariable` is: a * diagnostic that names the wrong variable sends an operator to fix something * they never set. */ export interface Config { /** SpecGuard deployment root, trailing slash stripped. `undefined` when unset. */ readonly endpoint: string | undefined; /** * WHICH variable the endpoint was read from — so a message about a bad value * can name the variable the operator actually set. * * Without this, someone who followed the brief and set `SPECGUARD_URL` to a * malformed value would be told to go and fix `SPECGUARD_ENDPOINT`, which they * never set. Accepting two spellings is only a kindness if the diagnostics * speak the one that was used. */ readonly endpointVariable: EndpointVariable | undefined; /** An `sgk_…` repository API key, from `SPECGUARD_API_KEY`. `undefined` when unset. */ readonly apiKey: string | undefined; /** * An `sgu_…` user API key, from `SPECGUARD_USER_API_KEY`. `undefined` when unset. * * A SECOND slot rather than a second meaning for the first one — see the note * at the top of this file. It is minted from the account page * (`/account`) and speaks for a person; the repository key is minted per * repository and speaks for one repository. Neither deployment endpoint will * accept the other's token, so the two values live in two places here too. */ readonly userApiKey: string | undefined; /** * An `sga_…` agent API key, from `SPECGUARD_AGENT_API_KEY`. `undefined` when unset. * * A THIRD slot, and the same argument as the second one — see the note at the * top of this file. It is minted from the account page's Agent keys panel * (`/account`) and speaks for NOBODY: its reach is the repository set and * permission set granted onto it at mint time, which is what makes it the * credential an automated agent can hold without borrowing a person's rights. * Neither deployment endpoint will accept the other token's kind, so the * three values live in three places here too. */ readonly agentApiKey: string | undefined; /** * The command that runs the `@intent` linter, already tokenised. * * Defaults to `["specguard-lint"]` (the Ruby client; a JS/TS project sets * `npx -p @yatfa/specguard specguard lint`). Most Ruby projects need the gem resolved * through their bundle, which is a deployment fact this server cannot guess — * set `SPECGUARD_LINT_COMMAND="bundle exec specguard-lint"` for those. It is * tokenised here rather than handed to a shell, so nothing an agent passes as * a tool argument can reach one. */ readonly lintCommand: readonly string[]; /** How long an HTTP call to SpecGuard may take, in milliseconds. */ readonly requestTimeoutMs: number; } export declare const DEFAULT_LINT_COMMAND: readonly string[]; export declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000; /** Reads config from an environment. Never throws — see the note above. */ export declare function loadConfig(env?: NodeJS.ProcessEnv): Config; /** * What a tool that talks to the SpecGuard API needs, once both halves are known. * * `apiKey` and `credential` are OPTIONAL here, and that is the credential-free * ask speaking (`requireEndpointApiConfig` below): an unauthenticated endpoint * needs an endpoint and no key, so the type a transport function accepts cannot * demand a credential the request will never present. `undefined` is a * first-class value, never a gap — the transport reads it as "send no * `Authorization` header", and `describeFailure` reads it as "this ask cannot * have a key problem". * * Every credentialled `require*` helper returns the NARROWED shape, * `CredentialledApiConfig` — the same object with both halves bound, which is * what they have always built. Narrowing in the return type rather than keeping * `ApiConfig` strict is what lets the optional fields exist without loosening * the 18 existing tools: a caller holding a credentialled config still knows * `apiKey` is a `string` and `credential` is a `Credential`, and TypeScript * keeps enforcing that where it is true. */ export interface ApiConfig { readonly endpoint: string; /** * WHICH variable `endpoint` came from, carried one layer further out. * * `requireHttpUrl` already names the variable the operator set when it refuses * a malformed value, but the HTTP client one level down has its own diagnostics * — unreachable, a body that is not JSON, and any 404 whose body is not * SpecGuard's own not-found shape — and each of them tells the operator to go * and check the endpoint; a 404 carrying SpecGuard's `{error: "not_found"}` * body surfaces SpecGuard's own sentence verbatim instead (SPGD-1146), naming * the cause rather than the endpoint. Without the name here, all three said * `SPECGUARD_ENDPOINT` unconditionally, so someone who followed the brief and * set `SPECGUARD_URL` was sent to fix a variable they never set. Threading it * onto `ApiConfig` rather than re-deriving it means every HTTP-backed tool * added later inherits correct naming the same way it inherits the URL check. */ readonly endpointVariable: EndpointVariable; /** * The key this request presents, when it presents one. * * `undefined` means the request presents NO `Authorization` header — never * "a key we forgot to fill in". Every credentialled helper binds a `string` * here (see `CredentialledApiConfig`), so for the 18 existing tools nothing * about the wire changes. */ readonly apiKey: string | undefined; /** * WHICH credential `apiKey` is — the variable it was read from, the prefix * that variable is expected to hold, and how to say both in a sentence. * * Exactly the treatment `endpointVariable` above gets, applied to the other * half, and for the same reason. `describeFailure` in * `support/specguard-api.ts` used to hardcode "SPECGUARD_API_KEY must be an * sgk_… key … keys are per-repository" into every 401, so the first * user-scoped tool routed through `getJson` would have inherited three * sentences that are all false of it — naming a variable its operator may * never have set. Carrying the answer rather than re-deriving it per tool is * what makes the next credential-scoped tool inherit correct naming the same * way it inherits the URL check. Optional here for exactly that reason and * no other: a credential-free ask has no credential to describe, and * inventing one would invent a requirement the deployment does not have. */ readonly credential: Credential | undefined; readonly requestTimeoutMs: number; } /** * `ApiConfig` with BOTH halves bound — the shape every credentialled `require*` * helper has always returned, now said in the type. * * A NARROWING, not a parallel type: `CredentialledApiConfig` extends * `ApiConfig`, so it flows into every transport function unchanged, while a * caller holding one still knows `apiKey` is a `string` and `credential` is a * `Credential` — the guarantee every credentialled call site and test relies * on. Only `requireEndpointApiConfig` returns the wider shape, and only the * transport reads the two optional fields there. */ export interface CredentialledApiConfig extends ApiConfig { readonly apiKey: string; readonly credential: Credential; } export type EndpointVariable = "SPECGUARD_ENDPOINT" | "SPECGUARD_URL"; export type ApiKeyVariable = "SPECGUARD_API_KEY" | "SPECGUARD_USER_API_KEY" | "SPECGUARD_AGENT_API_KEY"; /** * One of SpecGuard's three credential kinds, described well enough that a message * about it can be written without knowing which one it is. * * The two prose fields are sentence FRAGMENTS rather than whole messages on * purpose: the surrounding wording — "is not set in the MCP server's * environment", "SpecGuard rejected the API key (401)" — is the same for every * kind and is written once, at the site that knows the situation. Only the * parts that genuinely differ between an `sgk_` key, an `sgu_` key and an * `sga_` key live here. */ export interface Credential { /** The environment variable this kind of key is read from. */ readonly variable: ApiKeyVariable; /** The prefix SpecGuard requires of it, checked before any table is read. */ readonly prefix: "sgk_" | "sgu_" | "sga_"; /** Completes "… issued from ___" in a message about the variable being unset. */ readonly issuedFrom: string; /** Completes "… issued by ___" in a message about a 401. */ readonly rejection: string; } /** * The `sgk_` key: one repository, and the same variable CI already sets. * * The 401 wording states what is true of this kind: keys are per-repository, * and a revoked key's 401 identifies the revocation rather than reading as a * wrong key. The other two credential kinds carry their own refusals. */ export declare const REPOSITORY_CREDENTIAL: Credential; /** * The `sgu_` key: a person, and a variable nothing else in the toolchain reads. * * `specguard-ruby` ships runs with an `sgk_` key and has no notion of this one, * so an operator who already has CI reporting to SpecGuard does NOT already * have this variable — which is why the message says where to mint one rather * than assuming it is lying around. */ export declare const USER_CREDENTIAL: Credential; /** * The `sga_` key: an AGENT, and a credential that speaks for nobody. * * Like the `sgu_` key it is minted from the account page, but the two are not * the same gesture and the message must not blur them: a person key speaks with * the person's whole grantable surface, while an agent key reaches exactly the * repository set and permission set granted onto it at mint time — which is the * whole reason an automated agent holds this one rather than a person's. The * `issuedFrom` fragment names the panel, because an operator sent to "the * account page" for an `sga_` key lands on the person-key panel one scroll * earlier and mints the wrong kind. * * The `rejection` fragment names the two refusals specific to this kind: the * mutual prefix refusal (both other key kinds are refused here without a * lookup), and the fact that a key whose grant was too narrow does NOT read as * a 401 — a repository outside the key's set answers 404, deliberately * indistinguishable from a nonexistent one, so a 401 here is about the KEY * itself (wrong kind, revoked, or the owner archived) and never about which * repositories it names. */ export declare const AGENT_CREDENTIAL: Credential; /** * What a tool needing the `sgk_` REPOSITORY key requires — the endpoint and * `SPECGUARD_API_KEY`. * * Unchanged in name and in behaviour: every tool that reached the deployment * before this file grew a second credential calls exactly this, and gets exactly * what it got. */ export declare function requireApiConfig(config: Config): CredentialledApiConfig; /** * What a tool needing the `sgu_` USER key requires — the endpoint and * `SPECGUARD_USER_API_KEY`. * * A sibling rather than a flag on `requireApiConfig`, which is the shape * `loadConfig`'s note at the top of this file already sanctioned: a tool asks * for what IT needs, so a tool needing neither key is unaffected and startup * still validates nothing. The two differ only in which value and which * `Credential` they pass to the one implementation below, so a fix to the * diagnostics reaches both and they cannot drift into describing the same * situation differently. */ export declare function requireUserApiConfig(config: Config): CredentialledApiConfig; /** * What a tool needing the `sga_` AGENT key requires — the endpoint and * `SPECGUARD_AGENT_API_KEY`. * * The third sibling over the one shared body, for the reason the file's own * header anticipated: a tool asks for what IT needs, startup still validates * nothing, and a fix to the diagnostics reaches all three because they cannot * drift into describing the same situation differently. A repository-scoped * tool asked about a repository its `sgk_` slot does not name calls this, and * reads the plural endpoints the agent credential is served by. */ export declare function requireAgentApiConfig(config: Config): CredentialledApiConfig; /** * What a tool EITHER credential serves requires — the endpoint and at least one * of `SPECGUARD_AGENT_API_KEY` / `SPECGUARD_USER_API_KEY`. * * The plural endpoints accept both key kinds and bound the answer by whichever * was presented — the person's `accessible_by` set for an `sgu_` key, the key's * own granted set for an `sga_` one — so the tool's question ("which * repositories may I ask about") has a true answer under either credential, and * refusing until the operator picks one would be inventing a requirement the * deployment does not have. * * When BOTH are set, the agent key wins, and the reason is consistency of * scope rather than preference: every other agent-keyed tool answers inside the * key's granted set, so a listing served from the person's wider set would * advertise repositories the agent then cannot open — discovery promising more * than the tools that follow it can deliver. The agent set is a subset of its * owner's by mint-time validation, so this never widens what the answer names, * only keeps it to what the agent can actually act on. The variable is new, so * no existing operator's behavior changes: today nobody sets it, and setting it * is a deliberate act this precedence honours. * * When NEITHER is set, the one-message rule applies at double width: both * variables are named, because telling an operator who is expected to choose * between two spellings about only one of them would have them fix a variable, * re-call, and be told about the other — the round-trip waste the shared body's * note above exists to prevent. That sentence is NOT written here: the helper * falls through to the shared body with the credentials it should name, so the * wording is produced once, by the same code the other two entry points use, * and the one invariant this file asserts about its diagnostics — a fix reaches * every caller because no caller describes the situation itself — holds for * this helper too. The endpoint joins the same sentence when it is missing too. */ export declare function requireUserOrAgentApiConfig(config: Config): CredentialledApiConfig; /** * What a tool that needs NO credential requires — just the endpoint. * * The fourth sibling, and the first whose answer carries no key and no * `Credential`: the server's root-level `GET /version` (SPGD-1197, specguard * 08ab408) answers unauthenticated BY DESIGN — the platform's own doctrine * places it outside the credential seam, at the root where the no-account * reads (`/up`, the schema mirror) already live. SPGD-1331 made it serve a * second such ask: the schema mirror named there is now wrapped too * (`get_intent_schema`), through this same helper. Demanding a key here would * invent a requirement the deployment does not have, and the * credential-free config this returns is what keeps the transport from * sending an `Authorization` header at all. * * The endpoint is REQUIRED and PARSED through the shared `requireHttpUrl`, * exactly as the credentialled body does — a malformed value is the same * config typo with the same legible answer, and a helper that skipped the * parse would defer it to a bare `TypeError` at request time, read by the * server's error boundary as a defect in the bridge. * * The missing-endpoint message is its OWN sentence rather than the shared * body's, and that is deliberate: the shared body's sentence exists to name * the key variables an operator must choose between, and this ask has none — * folding it through would produce a message demanding a key nobody needs to * set. One sentence, naming only the endpoint variable the operator can fix. */ export declare function requireEndpointApiConfig(config: Config): ApiConfig; /** * Splits a configured command into argv WITHOUT a shell, honouring single and * double quotes so a path with a space survives. * * A shell is not used anywhere in this server, and this function is why it does * not need to be: `spawn` receives a program and a list, so no argument — least * of all a file path an agent passed to a tool — is ever parsed as syntax. */ export declare function tokenise(raw: string | undefined): string[];