Skip to content

harness/core

The harness’s own core — path resolution, guard state, and the web-search seam’s vocabulary.

⚠ Schemas are NOT declared here. Every TypeBox schema in this repo lives in @enso/core, so that a shape is either declared there or does not exist. Re-exporting them from here would defeat that: a consumer could import GuardPolicy from two places and neither import would look wrong.

Import schemas from @enso/core directly. Import path helpers from here.

⚠ THE LIST IS WHAT THE EXTENSIONS IMPORT, AND NOTHING ELSE (#263). It used to be the modules’ full public surface, which is a different claim and an unfalsifiable one: 17 names were exported here and imported nowhere, and they sat in fallow-baseline.json — a suppression whose own note said it was waiting on #173’s zone rule, which shipped without taking it. A barrel entry nothing imports is not an inventory, it is a name a reader must check before concluding that removing something is safe.

So the surface is derived from consumption and test/harness-barrel-gate.test.ts holds the two directions: a name here that no extension imports is red, and an extension importing a name that is not here does not compile. Internal helpers keep living in their modules — shell-segments.ts and web-search/anthropic-wire.ts are imported by their siblings and by their own tests, which is what “internal” means. Adding one back here is adding a consumer, and the gate asks for it.

Defined in: harness/core/web-search/vocabulary.ts:79

Thrown by provider runs on failure.

requestDispatched says whether the HTTP request left the process — an honest “may have billed” bit (whether a failed request billed is the provider’s accounting, not knowable here), surfaced when the fallback re-searches.

  • Error

new WebSearchProviderError(providerId, message, options): WebSearchProviderError

Defined in: harness/core/web-search/vocabulary.ts:83

WebSearchProviderId

string

boolean

WebSearchProviderError

Error.constructor

readonly providerId: WebSearchProviderId

Defined in: harness/core/web-search/vocabulary.ts:80

readonly requestDispatched: boolean

Defined in: harness/core/web-search/vocabulary.ts:81


Defined in: harness/core/web-search/providers/deepseek.ts:37

apiKey: string | undefined

Defined in: harness/core/web-search/providers/deepseek.ts:40

The key read at the composition root — never from config, never logged.

fetchUrl: FetchUrl

Defined in: harness/core/web-search/providers/deepseek.ts:41

optional onProgress?: (text) => void

Defined in: harness/core/web-search/providers/deepseek.ts:43

string

void

query: string

Defined in: harness/core/web-search/providers/deepseek.ts:38

signal: AbortSignal | undefined

Defined in: harness/core/web-search/providers/deepseek.ts:42


Defined in: harness/core/web-search/vocabulary.ts:100

The init the fetch seam takes — a fetch init narrowed to what a provider run actually sets.

Named (not inline) so a fake in a test and the composition root’s real fetch wrapper can both annotate the parameter instead of leaning on inference.

optional body?: string

Defined in: harness/core/web-search/vocabulary.ts:104

optional headers?: Record<string, string>

Defined in: harness/core/web-search/vocabulary.ts:103

optional method?: string

Defined in: harness/core/web-search/vocabulary.ts:101

optional redirect?: "manual"

Defined in: harness/core/web-search/vocabulary.ts:102

optional signal?: AbortSignal

Defined in: harness/core/web-search/vocabulary.ts:105


Defined in: harness/core/web-search/vocabulary.ts:153

What a MODEL-ROUTED provider run receives (anthropic / google / openai).

fetchUrl: FetchUrl

Defined in: harness/core/web-search/vocabulary.ts:157

model: SearchModel

Defined in: harness/core/web-search/vocabulary.ts:155

optional onProgress?: (text) => void

Defined in: harness/core/web-search/vocabulary.ts:160

Streaming progress for the UI; carries the accumulated answer text so far.

string

void

query: string

Defined in: harness/core/web-search/vocabulary.ts:154

resolveAuth: (model) => Promise<ResolvedProviderAuth>

Defined in: harness/core/web-search/vocabulary.ts:156

SearchModel

Promise<ResolvedProviderAuth>

signal: AbortSignal | undefined

Defined in: harness/core/web-search/vocabulary.ts:158


Defined in: harness/core/web-search/vocabulary.ts:136

The model the conversation runs on, as the PROVIDER SEAM sees it (L6 closed, #162): the seven fields the providers read, declared here as ours.

The runtime’s own model record is converted once, at the extension (extensions/web-search/index.ts#searchModelOf), and nothing under harness/core imports the runtime’s type for it any more. A provider that needs an eighth field adds it here, where the seam is, not by reaching for the vendor’s.

api: string

Defined in: harness/core/web-search/vocabulary.ts:139

The wire API the model speaks — anthropic-messages, openai-responses, … — which routes the search.

baseUrl: string

Defined in: harness/core/web-search/vocabulary.ts:142

optional headers?: Record<string, string>

Defined in: harness/core/web-search/vocabulary.ts:145

id: string

Defined in: harness/core/web-search/vocabulary.ts:137

maxTokens: number

Defined in: harness/core/web-search/vocabulary.ts:144

provider: string

Defined in: harness/core/web-search/vocabulary.ts:141

The provider’s id, which names the environment key that may authenticate it.

reasoning: boolean

Defined in: harness/core/web-search/vocabulary.ts:143


Defined in: harness/core/web-search/providers/tavily.ts:29

apiKey: string

Defined in: harness/core/web-search/providers/tavily.ts:31

optional excludeDomains?: readonly string[]

Defined in: harness/core/web-search/providers/tavily.ts:33

fetchUrl: FetchUrl

Defined in: harness/core/web-search/providers/tavily.ts:34

optional includeDomains?: readonly string[]

Defined in: harness/core/web-search/providers/tavily.ts:32

query: string

Defined in: harness/core/web-search/providers/tavily.ts:30

signal: AbortSignal | undefined

Defined in: harness/core/web-search/providers/tavily.ts:35


ResolvedProviderAuth = { apiKey?: string; baseUrl?: string; headers?: Record<string, string>; ok: true; } | { error: string; ok: false; }

Defined in: harness/core/web-search/vocabulary.ts:121

Auth for one model-routed request, as pi’s model registry resolves it (plus the provider-env fallback — see auth.ts).


WebSearchOutcome = { answerText?: string; kind: "searched"; providerId: WebSearchProviderId; snippets?: WebSearchSnippet[]; sources: WebSource[]; } | { answerText: string; kind: "ungrounded"; providerId: WebSearchProviderId; }

Defined in: harness/core/web-search/vocabulary.ts:53

What one provider run produced — the ACCURATE outcome union (#63, Daniel’s requirement).

⚠ searched vs ungrounded is keyed on evidence that a native search actually ran (a server_tool_use block on the Anthropic wire; each provider names its own signal), NEVER on source count: a real search can return zero results, and an ungrounded answer citing a hallucinated URL would read as grounded. Upstream’s grounded = sources.length > 0 had exactly that defect.

{ answerText?: string; kind: "searched"; providerId: WebSearchProviderId; snippets?: WebSearchSnippet[]; sources: WebSource[]; }

optional answerText?: string

The provider-synthesized answer, citation-marked. Absent for retrieval backends (Tavily).

kind: "searched"

providerId: WebSearchProviderId

optional snippets?: WebSearchSnippet[]

Retrieval results with snippets — the Tavily shape; model-native backends leave it absent.

sources: WebSource[]


{ answerText: string; kind: "ungrounded"; providerId: WebSearchProviderId; }

answerText: string

kind: "ungrounded"

The model answered WITHOUT searching — its own knowledge, not the live web.

providerId: WebSearchProviderId


WebSearchProviderId = "anthropic" | "google" | "openai" | "deepseek" | "tavily"

Defined in: harness/core/web-search/vocabulary.ts:18

Every backend that can answer a web_search call.


const DEEPSEEK_API_KEY_VARIABLE: "DEEPSEEK_API_KEY" = 'DEEPSEEK_API_KEY'

Defined in: harness/core/web-search/providers/deepseek.ts:30

The env var holding the key — the SAME name pi-ai’s own provider-env map uses.


const TAVILY_API_KEY_VARIABLE: "TAVILY_API_KEY" = 'TAVILY_API_KEY'

Defined in: harness/core/web-search/providers/tavily.ts:24

The env var that ARMS the fallback — read at the composition root only.


resolveModelAuth(authSource, model, readProviderEnvironmentKey): Promise<ResolvedProviderAuth>

Defined in: harness/core/web-search/auth.ts:64

readProviderEnvironmentKey is a SEAM (PR #64 review): it defaults to pi-ai’s getEnvApiKey, which reads the launching process environment — injectable so the fallback’s POSITIVE path is testable without a test ever touching process.env.

ModelAuthSource

SearchModel

(provider) => string | undefined

Promise<ResolvedProviderAuth>


routedProviderIdForModel(model): "anthropic" | "google" | "openai" | undefined

Defined in: harness/core/web-search/vocabulary.ts:172

Which model-routed provider serves a model, by its API kind.

undefined means no native search path exists for it — selection falls through to the fallback chain. (pi registers DeepSeek models as openai-completions, so they land here as undefined too: the DeepSeek backend is config-pinned, never model-routed — #63.)

SearchModel

"anthropic" | "google" | "openai" | undefined


runAnthropicProviderSearch(request): Promise<WebSearchOutcome>

Defined in: harness/core/web-search/providers/anthropic.ts:48

ModelSearchRequest

Promise<WebSearchOutcome>


runDeepseekProviderSearch(request): Promise<WebSearchOutcome>

Defined in: harness/core/web-search/providers/deepseek.ts:47

DeepseekSearchRequest

Promise<WebSearchOutcome>


runGoogleProviderSearch(request): Promise<WebSearchOutcome>

Defined in: harness/core/web-search/providers/google.ts:99

ModelSearchRequest

Promise<WebSearchOutcome>


runOpenAiProviderSearch(request): Promise<WebSearchOutcome>

Defined in: harness/core/web-search/providers/openai.ts:175

ModelSearchRequest

Promise<WebSearchOutcome>


runTavilyProviderSearch(request): Promise<WebSearchOutcome>

Defined in: harness/core/web-search/providers/tavily.ts:39

TavilySearchRequest

Promise<WebSearchOutcome>

isFetchableContentType(contentTypeHeader): boolean

Defined in: harness/core/web-fetch-policy.ts:87

True for response bodies worth returning as text.

An ABSENT Content-Type header is refused — web_fetch is a docs-reader, and “the server did not say” fails closed.

string | null

boolean


isHostAllowed(hostname, allowedHosts): boolean

Defined in: harness/core/web-fetch-policy.ts:69

Exact, case-insensitive hostname match.

No wildcards and no subdomain inheritance, deliberately: one allowlist entry is one reviewable grant, and api.example.com is a different grant from example.com. Ports are not part of the match — the protocol gate already confines the scheme, and a host grant is a host grant on any port.

string

readonly string[]

boolean


isPublicIpAddress(address): boolean

Defined in: harness/core/web-fetch-policy.ts:162

ADVISORY public-address classifier — see the module header’s stated non-coverage.

Anything not provably public (unparseable included) classifies as non-public: the check gates a refusal, so unknown must fail closed.

string

boolean


parseWebFetchUrl(rawUrl): WebFetchUrlDecision

Defined in: harness/core/web-fetch-policy.ts:37

Parse and gate a raw URL string. Refusal reasons are written for the model to read.

string

WebFetchUrlDecision

canonicalToolName(name): string

Defined in: harness/core/tool-identity.ts:18

Canonical tool identity (#4, gap 2).

pi’s built-in tools are lowercase (bash, write), but Claude-Code-style vendor packages register capitalised names — picc-permission-modes’ own types carry tool: "Bash" — so two naming conventions are live in one process. A case-sensitive comparison against a policy name silently misses the variant: the call proceeds, the guard never fires, and nothing reports the miss. That is the silence-as-refusal class this module closes.

Every comparison of a tool name to a policy name goes through this fold — on BOTH sides — so a case variant can never skip a guard branch. One function, no mapping table: the only consumer today is our own comparisons, and the picc-name translation table belongs to the allow-store work deferred with the #4 arbiter.

string

string


createSessionReadTracker(): SessionReadTracker

Defined in: harness/core/read-tracker.ts:96

SessionReadTracker


findDeniedNetworkEgress(command, deniedBinaries, options?): NetworkEgressMatch | undefined

Defined in: harness/core/network-egress.ts:810

Find the first denied network egress in a shell command, or undefined when none match. coarse forces the raw word-boundary scan (the PowerShell path).

string

readonly string[]

boolean

NetworkEgressMatch | undefined


findEnvironmentDump(command, options?): EnvironmentDumpMatch | undefined

Defined in: harness/core/environment-dump.ts:183

Find the first environment dump in a shell command, or undefined when none.

coarse forces the raw scan (the PowerShell path); a POSIX command the segmenter cannot parse takes it too.

string

boolean

EnvironmentDumpMatch | undefined


findSecretPathWord(command, fragments, cwd, options?): SecretPathMatch | undefined

Defined in: harness/core/shell-secret-paths.ts:155

Find the first word of a shell command that names credential material, or undefined when none does. coarse forces the text pass (the PowerShell path).

string

readonly string[]

string

boolean

SecretPathMatch | undefined


isInsideAnyRoot(absolutePath, roots): boolean

Defined in: harness/core/paths.ts:162

True when the path is inside at least one root, checking both the literal path and its realpath.

string

readonly string[]

boolean


isPersistenceWriteTarget(absolutePath): boolean

Defined in: harness/core/paths.ts:252

True when writing the path would plant a persistence vector (#21) — a shell-init file, git/agent/editor config, or anything inside such a directory.

Two candidates are checked: the literal path, and the realpath of its nearest existing ancestor. The second is what catches symlinks — an existing symlink named notes.txt pointing at ~/.bashrc resolves to the true basename, and a NEW file created through a symlinked directory (innocent/ → ~/.claude/) resolves to a path whose segments carry the dangerous directory. (A new file’s own basename never changes through a directory symlink, so the literal candidate covers that half.)

⚠ Basenames match EXACTLY (case-insensitive, for case-folding filesystems), never as fragments — .profile-notes/x.ts and my.bashrc must not be caught.

string

boolean


isProtectedForWrite(absolutePath, fragments): boolean

Defined in: harness/core/paths.ts:232

True when the path contains a fragment that is never writable.

string

readonly string[]

boolean


looksLikeSecret(absolutePath, fragments): boolean

Defined in: harness/core/paths.ts:219

True when the path looks like credential material, by directory fragment, by basename, or because it is a process’s environment.

⚠ Both the literal path AND the realpath of its nearest existing ancestor are judged, for the reason isPersistenceWriteTarget and isInsideAnyRoot already do it: a symlink inside the workspace named notes.txt pointing at ~/.aws/credentials has an innocent literal path, and pi follows the link. Reading a secret is the precondition for exfiltrating one, so this gate cannot be the one that judges only the name it was given.

string

readonly string[]

boolean


resolveAgainstCwd(candidate, cwd): string

Defined in: harness/core/paths.ts:110

Resolve a tool-supplied path against the session cwd, to the SAME absolute path pi’s own tool will operate on. Does NOT confine — see isInsideAnyRoot.

Mirrors pi’s resolveToCwd → resolvePath: the candidate is normalised with the tool options (see normalizeLikePi); the cwd is normalised with pi’s DEFAULT options (tilde and file:// only — pi neither strips @ from a base directory nor collapses its unicode spaces).

string

string

string

runUnderDeadline<T>(callerSignal, deadline, run): Promise<T>

Defined in: harness/core/deadline.ts:17

Run work under a deadline AND the caller’s abort signal, as one signal (#174; the shape web_fetch and web_search each carried).

Two things every copy had to get right:

  • A signal that is ALREADY aborted never fires its event (addEventListener is not retroactive), so an execute() entered post-abort would run to the deadline — the guard aborts first (PR #58 review, nit 3).
  • A budget already spent aborts BEFORE the work starts: a 0 ms timer would let fast work answer first, and a fallback must not start what the deadline already forbade.

The deadline’s reason is the caller’s — it names the tool and the budget in the refusal.

T

AbortSignal | undefined

() => Error

The error the work sees when the deadline, not the caller, ended it.

number

Milliseconds left; zero or less aborts before run.

(signal) => Promise<T>

Promise<T>