/** * Thread namespace resolution (task 097 P2). * * A namespace name is a thread-name prefix: namespace `pursuit` covers every * `pursuit-*` thread but NOT bare `pursuit`, which stays the owner's own work * thread. These helpers drive list visibility across every surface (REST * `/api/threads`, webchat WS thread list, `/api/agents`) so the rule is defined * exactly once. * * Two scope sources, one predicate: * - `/api/threads` + webchat WS → scope from the presented API key * (`namespaceForKey`). * - `/api/agents` (`list_agents`) → scope from the calling thread's own name * (`namespaceForThread`), because the MCP server authenticates with the * gateway's unscoped key. * * Visibility and access are DIFFERENT predicates (task 097 P7) — see * `threadVisibleToScope` vs `threadAccessibleToScope`. A scoped key is confined * to its namespace (access) and enumerates NOTHING (visibility): thread names * carry the per-visitor entropy, so a scoped key grants access to a namespace, * not knowledge of it — the capability-by-name model. The admin key reaches * everything but omits visitors from lists (P2 keeps the owner's list clean). * Explicit isolated profiles are visitors regardless of their name (task 220). */ import type { McpServerEntry } from '../lib/gateway.js'; import { type LicenseStatus } from '../lib/license.js'; import type { NamespaceConfig } from './config.js'; /** * Does `threadName` belong to namespace `ns`? True only for `${ns}-` prefixed * children — so `pursuit` covers `pursuit-abc` but NOT bare `pursuit`, and not * `pursuitx`. * * The base name is deliberately excluded: `pursuit` is the *owner's* work thread * (deployments, product strategy) and must stay in the owner's default list, * while the app-minted `pursuit-` sessions are what the namespace hides. * Every agent-aware app has this shape — a human-facing project thread plus the * instance threads the app mints while functioning. */ export declare function threadInNamespace(threadName: string, ns: string): boolean; /** * The namespace a thread belongs to, or undefined for a default (non-namespaced) * thread. When multiple namespaces match (overlapping prefixes), the * longest-matching name wins so the most specific namespace is chosen. */ export declare function namespaceForThread(threadName: string, namespaces: NamespaceConfig[]): string | undefined; /** * The namespace an API key is scoped to, or undefined for an unscoped * (default/admin) key. */ export declare function namespaceForKey(key: string, namespaces: NamespaceConfig[]): string | undefined; /** * Is `threadName` LISTED for a viewer whose scope is `scopeNs`? * - scopeNs set (scoped key/agent): sees NOTHING (task 097 P7). Instance * threads are capability names — `pursuit-` is the per-visitor * secret, so letting one visitor's key enumerate its siblings would hand out * every other visitor's capability. A scoped caller must already KNOW its * thread's name; it never needs a list. * - scopeNs undefined (default/admin): sees every thread EXCEPT namespaced ones * — which includes each namespace's base thread (`pursuit`), since that one * belongs to the owner, not the namespace. * - An isolated visitor profile is always hidden, regardless of name or * co-thread membership. Direct access remains a separate check. */ export declare function threadVisibleToScope(threadName: string, scopeNs: string | undefined, namespaces: NamespaceConfig[]): boolean; /** * May a viewer whose scope is `scopeNs` ACT ON `threadName` — read its history, * post to it, edit its config, delete it (task 097 P7)? * * - scopeNs set (scoped key): ONLY its own namespace's threads. A scoped key is * published to a browser (Pursuit prints one on a public page), so it must be * a real boundary, not a list filter. * - scopeNs undefined (admin key): everything, INCLUDING namespaced threads. * * Deliberately NOT `threadVisibleToScope`: that one hides namespaced threads * from the admin *list* (P2) and hides everything from a scoped key * (no-enumeration). Reusing it here would lock the admin key — and every * unscoped internal caller (`send_to_agent`, the queue drain, MCP shims) — out * of the very threads it administers, and lock scoped keys out of their own * threads. Listing and acting are independent axes. */ export declare function threadAccessibleToScope(threadName: string, scopeNs: string | undefined, namespaces: NamespaceConfig[]): boolean; /** Existing threads belonging to `ns` (longest-match, so `x-v-1` counts under `x-v`). */ export declare function countNamespaceThreads(ns: string, existingThreadNames: string[], namespaces: NamespaceConfig[]): number; export interface DemoCapDenial { namespace: string; count: number; cap: number; } /** * Demo-mode capacity check (task 127). Returns a denial when a request would * MINT a new thread in an unlicensed namespace that is already at the cap, or * undefined when the request may proceed. * * Three deliberate exemptions, in the order they are checked: * 1. **Non-namespaced threads are never gated.** Personal threads, the CLI, the * TUI — the gate exists only where the sold capability lives. * 2. **Threads that already exist always keep working.** The cap gates minting, * never reads or replies, so an evaluator who reaches it loses no data. * 3. **Licensed namespaces are uncapped.** * * Pure: the caller supplies the thread inventory, so this is testable without a * filesystem and callable from both the REST and WS gates. */ export declare function demoMintDenial(threadName: string, namespaces: NamespaceConfig[], existingThreadNames: string[], status: LicenseStatus, cap?: number): DemoCapDenial | undefined; /** * The startup / periodic licence banner (task 127), as lines. Pure so the * wording is asserted in tests rather than eyeballed in a log. * * A licensed gateway gets exactly one confirmation line and nothing further. An * unlicensed one gets one line per configured namespace naming its occupancy, * plus the reason and the contact address — so an operator who is about to hit * the cap sees it coming rather than discovering it as a 402. */ export declare function licenseBannerLines(status: LicenseStatus, namespaces: NamespaceConfig[], existingThreadNames: string[], cap?: number): string[]; /** * Every API key scoped to any namespace, for folding into the gateway's set of * valid authentication keys (a scoped key must still authenticate). */ export declare function allNamespaceKeys(namespaces: NamespaceConfig[]): string[]; /** * The extra MCP servers a thread's turn should spawn, or undefined when its * namespace configures none (task 097 P5). Scoped exactly like `executorProxy`: * only `${ns}-*` threads get the namespace's servers, so one app's tools never * leak into unrelated threads — and the owner's bare `pursuit` work thread * doesn't get them either (P2 amendment). * * This is the only place the rule lives; every pipeline construction site (SSE * turn, WS turn, agent inject, queue drain, webhook injection) calls it with the * thread name it already has. */ export declare function extraMcpServersForThread(threadName: string, namespaces: NamespaceConfig[]): Record | undefined; /** * Resolve the executor-proxy upstream for a request path, or undefined if no * namespace claims it (task 097 P3). A namespace's `executorProxy.pathPrefixes` * match the route path exactly or at a `${prefix}/…` segment boundary; the * longest matching prefix wins so nested prefixes resolve deterministically. * Presence of `executorProxy` alone activates the proxy — decoupled from * bridge.enabled and from the caller's namespace scope (any valid key). */ export declare function resolveExecutorProxy(routePath: string, namespaces: NamespaceConfig[]): { origin: string; } | undefined; //# sourceMappingURL=namespaces.d.ts.map