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.
Web search
Section titled “Web search”WebSearchProviderError
Section titled “WebSearchProviderError”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.
Extends
Section titled “Extends”Error
Constructors
Section titled “Constructors”Constructor
Section titled “Constructor”new WebSearchProviderError(
providerId,message,options):WebSearchProviderError
Defined in: harness/core/web-search/vocabulary.ts:83
Parameters
Section titled “Parameters”providerId
Section titled “providerId”message
Section titled “message”string
options
Section titled “options”requestDispatched
Section titled “requestDispatched”boolean
Returns
Section titled “Returns”Overrides
Section titled “Overrides”Error.constructor
Properties
Section titled “Properties”providerId
Section titled “providerId”
readonlyproviderId:WebSearchProviderId
Defined in: harness/core/web-search/vocabulary.ts:80
requestDispatched
Section titled “requestDispatched”
readonlyrequestDispatched:boolean
Defined in: harness/core/web-search/vocabulary.ts:81
DeepseekSearchRequest
Section titled “DeepseekSearchRequest”Defined in: harness/core/web-search/providers/deepseek.ts:37
Properties
Section titled “Properties”apiKey
Section titled “apiKey”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
Section titled “fetchUrl”fetchUrl:
FetchUrl
Defined in: harness/core/web-search/providers/deepseek.ts:41
onProgress?
Section titled “onProgress?”
optionalonProgress?: (text) =>void
Defined in: harness/core/web-search/providers/deepseek.ts:43
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”void
query:
string
Defined in: harness/core/web-search/providers/deepseek.ts:38
signal
Section titled “signal”signal:
AbortSignal|undefined
Defined in: harness/core/web-search/providers/deepseek.ts:42
FetchUrlInit
Section titled “FetchUrlInit”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.
Properties
Section titled “Properties”
optionalbody?:string
Defined in: harness/core/web-search/vocabulary.ts:104
headers?
Section titled “headers?”
optionalheaders?:Record<string,string>
Defined in: harness/core/web-search/vocabulary.ts:103
method?
Section titled “method?”
optionalmethod?:string
Defined in: harness/core/web-search/vocabulary.ts:101
redirect?
Section titled “redirect?”
optionalredirect?:"manual"
Defined in: harness/core/web-search/vocabulary.ts:102
signal?
Section titled “signal?”
optionalsignal?:AbortSignal
Defined in: harness/core/web-search/vocabulary.ts:105
ModelSearchRequest
Section titled “ModelSearchRequest”Defined in: harness/core/web-search/vocabulary.ts:153
What a MODEL-ROUTED provider run receives (anthropic / google / openai).
Properties
Section titled “Properties”fetchUrl
Section titled “fetchUrl”fetchUrl:
FetchUrl
Defined in: harness/core/web-search/vocabulary.ts:157
model:
SearchModel
Defined in: harness/core/web-search/vocabulary.ts:155
onProgress?
Section titled “onProgress?”
optionalonProgress?: (text) =>void
Defined in: harness/core/web-search/vocabulary.ts:160
Streaming progress for the UI; carries the accumulated answer text so far.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”void
query:
string
Defined in: harness/core/web-search/vocabulary.ts:154
resolveAuth
Section titled “resolveAuth”resolveAuth: (
model) =>Promise<ResolvedProviderAuth>
Defined in: harness/core/web-search/vocabulary.ts:156
Parameters
Section titled “Parameters”Returns
Section titled “Returns”Promise<ResolvedProviderAuth>
signal
Section titled “signal”signal:
AbortSignal|undefined
Defined in: harness/core/web-search/vocabulary.ts:158
SearchModel
Section titled “SearchModel”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.
Properties
Section titled “Properties”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
Section titled “baseUrl”baseUrl:
string
Defined in: harness/core/web-search/vocabulary.ts:142
headers?
Section titled “headers?”
optionalheaders?:Record<string,string>
Defined in: harness/core/web-search/vocabulary.ts:145
id:
string
Defined in: harness/core/web-search/vocabulary.ts:137
maxTokens
Section titled “maxTokens”maxTokens:
number
Defined in: harness/core/web-search/vocabulary.ts:144
provider
Section titled “provider”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
Section titled “reasoning”reasoning:
boolean
Defined in: harness/core/web-search/vocabulary.ts:143
TavilySearchRequest
Section titled “TavilySearchRequest”Defined in: harness/core/web-search/providers/tavily.ts:29
Properties
Section titled “Properties”apiKey
Section titled “apiKey”apiKey:
string
Defined in: harness/core/web-search/providers/tavily.ts:31
excludeDomains?
Section titled “excludeDomains?”
optionalexcludeDomains?: readonlystring[]
Defined in: harness/core/web-search/providers/tavily.ts:33
fetchUrl
Section titled “fetchUrl”fetchUrl:
FetchUrl
Defined in: harness/core/web-search/providers/tavily.ts:34
includeDomains?
Section titled “includeDomains?”
optionalincludeDomains?: readonlystring[]
Defined in: harness/core/web-search/providers/tavily.ts:32
query:
string
Defined in: harness/core/web-search/providers/tavily.ts:30
signal
Section titled “signal”signal:
AbortSignal|undefined
Defined in: harness/core/web-search/providers/tavily.ts:35
ResolvedProviderAuth
Section titled “ResolvedProviderAuth”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
Section titled “WebSearchOutcome”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.
Union Members
Section titled “Union Members”Type Literal
Section titled “Type Literal”{ answerText?: string; kind: "searched"; providerId: WebSearchProviderId; snippets?: WebSearchSnippet[]; sources: WebSource[]; }
answerText?
Section titled “answerText?”
optionalanswerText?:string
The provider-synthesized answer, citation-marked. Absent for retrieval backends (Tavily).
kind:
"searched"
providerId
Section titled “providerId”providerId:
WebSearchProviderId
snippets?
Section titled “snippets?”
optionalsnippets?:WebSearchSnippet[]
Retrieval results with snippets — the Tavily shape; model-native backends leave it absent.
sources
Section titled “sources”sources:
WebSource[]
Type Literal
Section titled “Type Literal”{ answerText: string; kind: "ungrounded"; providerId: WebSearchProviderId; }
answerText
Section titled “answerText”answerText:
string
kind:
"ungrounded"
The model answered WITHOUT searching — its own knowledge, not the live web.
providerId
Section titled “providerId”providerId:
WebSearchProviderId
WebSearchProviderId
Section titled “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.
DEEPSEEK_API_KEY_VARIABLE
Section titled “DEEPSEEK_API_KEY_VARIABLE”
constDEEPSEEK_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.
TAVILY_API_KEY_VARIABLE
Section titled “TAVILY_API_KEY_VARIABLE”
constTAVILY_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()
Section titled “resolveModelAuth()”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.
Parameters
Section titled “Parameters”authSource
Section titled “authSource”ModelAuthSource
readProviderEnvironmentKey
Section titled “readProviderEnvironmentKey”(provider) => string | undefined
Returns
Section titled “Returns”Promise<ResolvedProviderAuth>
routedProviderIdForModel()
Section titled “routedProviderIdForModel()”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.)
Parameters
Section titled “Parameters”Returns
Section titled “Returns”"anthropic" | "google" | "openai" | undefined
runAnthropicProviderSearch()
Section titled “runAnthropicProviderSearch()”runAnthropicProviderSearch(
request):Promise<WebSearchOutcome>
Defined in: harness/core/web-search/providers/anthropic.ts:48
Parameters
Section titled “Parameters”request
Section titled “request”Returns
Section titled “Returns”Promise<WebSearchOutcome>
runDeepseekProviderSearch()
Section titled “runDeepseekProviderSearch()”runDeepseekProviderSearch(
request):Promise<WebSearchOutcome>
Defined in: harness/core/web-search/providers/deepseek.ts:47
Parameters
Section titled “Parameters”request
Section titled “request”Returns
Section titled “Returns”Promise<WebSearchOutcome>
runGoogleProviderSearch()
Section titled “runGoogleProviderSearch()”runGoogleProviderSearch(
request):Promise<WebSearchOutcome>
Defined in: harness/core/web-search/providers/google.ts:99
Parameters
Section titled “Parameters”request
Section titled “request”Returns
Section titled “Returns”Promise<WebSearchOutcome>
runOpenAiProviderSearch()
Section titled “runOpenAiProviderSearch()”runOpenAiProviderSearch(
request):Promise<WebSearchOutcome>
Defined in: harness/core/web-search/providers/openai.ts:175
Parameters
Section titled “Parameters”request
Section titled “request”Returns
Section titled “Returns”Promise<WebSearchOutcome>
runTavilyProviderSearch()
Section titled “runTavilyProviderSearch()”runTavilyProviderSearch(
request):Promise<WebSearchOutcome>
Defined in: harness/core/web-search/providers/tavily.ts:39
Parameters
Section titled “Parameters”request
Section titled “request”Returns
Section titled “Returns”Promise<WebSearchOutcome>
Web fetch policy
Section titled “Web fetch policy”isFetchableContentType()
Section titled “isFetchableContentType()”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.
Parameters
Section titled “Parameters”contentTypeHeader
Section titled “contentTypeHeader”string | null
Returns
Section titled “Returns”boolean
isHostAllowed()
Section titled “isHostAllowed()”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.
Parameters
Section titled “Parameters”hostname
Section titled “hostname”string
allowedHosts
Section titled “allowedHosts”readonly string[]
Returns
Section titled “Returns”boolean
isPublicIpAddress()
Section titled “isPublicIpAddress()”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.
Parameters
Section titled “Parameters”address
Section titled “address”string
Returns
Section titled “Returns”boolean
parseWebFetchUrl()
Section titled “parseWebFetchUrl()”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.
Parameters
Section titled “Parameters”rawUrl
Section titled “rawUrl”string
Returns
Section titled “Returns”WebFetchUrlDecision
Guard predicates
Section titled “Guard predicates”canonicalToolName()
Section titled “canonicalToolName()”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.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”string
createSessionReadTracker()
Section titled “createSessionReadTracker()”createSessionReadTracker():
SessionReadTracker
Defined in: harness/core/read-tracker.ts:96
Returns
Section titled “Returns”SessionReadTracker
findDeniedNetworkEgress()
Section titled “findDeniedNetworkEgress()”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).
Parameters
Section titled “Parameters”command
Section titled “command”string
deniedBinaries
Section titled “deniedBinaries”readonly string[]
options?
Section titled “options?”coarse?
Section titled “coarse?”boolean
Returns
Section titled “Returns”NetworkEgressMatch | undefined
findEnvironmentDump()
Section titled “findEnvironmentDump()”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.
Parameters
Section titled “Parameters”command
Section titled “command”string
options?
Section titled “options?”coarse?
Section titled “coarse?”boolean
Returns
Section titled “Returns”EnvironmentDumpMatch | undefined
findSecretPathWord()
Section titled “findSecretPathWord()”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).
Parameters
Section titled “Parameters”command
Section titled “command”string
fragments
Section titled “fragments”readonly string[]
string
options?
Section titled “options?”coarse?
Section titled “coarse?”boolean
Returns
Section titled “Returns”SecretPathMatch | undefined
isInsideAnyRoot()
Section titled “isInsideAnyRoot()”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.
Parameters
Section titled “Parameters”absolutePath
Section titled “absolutePath”string
readonly string[]
Returns
Section titled “Returns”boolean
isPersistenceWriteTarget()
Section titled “isPersistenceWriteTarget()”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.
Parameters
Section titled “Parameters”absolutePath
Section titled “absolutePath”string
Returns
Section titled “Returns”boolean
isProtectedForWrite()
Section titled “isProtectedForWrite()”isProtectedForWrite(
absolutePath,fragments):boolean
Defined in: harness/core/paths.ts:232
True when the path contains a fragment that is never writable.
Parameters
Section titled “Parameters”absolutePath
Section titled “absolutePath”string
fragments
Section titled “fragments”readonly string[]
Returns
Section titled “Returns”boolean
looksLikeSecret()
Section titled “looksLikeSecret()”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.
Parameters
Section titled “Parameters”absolutePath
Section titled “absolutePath”string
fragments
Section titled “fragments”readonly string[]
Returns
Section titled “Returns”boolean
resolveAgainstCwd()
Section titled “resolveAgainstCwd()”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).
Parameters
Section titled “Parameters”candidate
Section titled “candidate”string
string
Returns
Section titled “Returns”string
Deadlines
Section titled “Deadlines”runUnderDeadline()
Section titled “runUnderDeadline()”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 (
addEventListeneris not retroactive), so anexecute()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.
Type Parameters
Section titled “Type Parameters”T
Parameters
Section titled “Parameters”callerSignal
Section titled “callerSignal”AbortSignal | undefined
deadline
Section titled “deadline”reason
Section titled “reason”() => Error
The error the work sees when the deadline, not the caller, ended it.
remainingMs
Section titled “remainingMs”number
Milliseconds left; zero or less aborts before run.
(signal) => Promise<T>
Returns
Section titled “Returns”Promise<T>