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:90

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:72

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:165

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:40

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

string

WebFetchUrlDecision

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

Where a credential fragment is placed to name a location (#160 §4).

readonly existing: readonly string[]

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

Places a fragment names a location in only once it EXISTS: the session’s workspace and the harness’s own root, where .enso/ lives. A search normally starts in them, so a location that is not there must not refuse it — grep path=. in every project would be.

readonly home: string

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

The user’s home: a fragment names a location here whether or not it exists yet.


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 packages register capitalised names (Bash, Write), so two naming conventions can be 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 a 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:104

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:181

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:315

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:295

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:230

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:129

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


secretLocationBeneath(absolutePath, fragments, places): string | undefined

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

The credential location a search starting at this directory would reach beneath it, if any: each fragment placed in one of CredentialPlaces (~/.ssh, ~/.aws, <harness>/.enso, …), and the directory an ancestor of it or the location itself (#160 §4).

looksLikeSecret judges the one path it is given, which is the right question for a tool that reads that path. A recursive search reads everything BENEATH it, so ~/.aws refused while ~ — and / — searched the same files. As looksLikeSecret does, both the literal directory and the realpath of its nearest existing ancestor are judged (a workspace symlink to home), against each place and its realpath.

⚠ The harness’s .enso/ is NOT protected by being gitignored: ripgrep honours .gitignore only inside a git work tree, and the agent may edit .gitignore (PR #420 review, measured).

⚠ WHAT THIS DOES NOT SEE, stated: a fragment’s location anywhere but those places (/srv/app/.aws), and a basename-class file (.env, id_rsa) beneath the directory — those are found only by walking the tree.

string

readonly string[]

CredentialPlaces

string | undefined

Defined in: harness/core/session-config.ts:44

The session’s context, as pi hands it to session_start.

isProjectTrusted(): boolean

Defined in: harness/core/session-config.ts:53

pi’s answer for this session: whether the project’s own scope is loaded.

boolean

readonly cwd: string

Defined in: harness/core/session-config.ts:46

readonly hasUI: boolean

Defined in: harness/core/session-config.ts:47

readonly home: string

Defined in: harness/core/session-config.ts:45

readonly hostOwnsTrust: boolean

Defined in: harness/core/session-config.ts:59

The web host decides trust (#502): it asks before a run, applies the answer to pi and reloads, so here the project’s config follows isProjectTrusted() and nothing is asked twice. The terminal has no host: there the question below is Enso’s own, as pi’s covers only .pi/.

readonly ui: object

Defined in: harness/core/session-config.ts:48

notify(message, type?): void

string

"info" | "warning" | "error"

void

select(title, options): Promise<string | undefined>

string

string[]

Promise<string | undefined>


Defined in: harness/core/session-config.ts:26

What a session’s tools read their policy from.

readonly path: string

Defined in: harness/core/session-config.ts:30

The file a human changes to change this session’s policy: the project’s, when it was read.

readonly result: EnsoConfigParseResult

Defined in: harness/core/session-config.ts:28

The merged config, or why it could not be read — a caller fails CLOSED on unreadable.

readonly sources: readonly string[]

Defined in: harness/core/session-config.ts:36

The config files this resolution read, user first; empty when none exists (#435). Recorded here, where the reading happens: the resolution is cached per session, so a probe at record time would name files the cached result never saw (PR #440 review).


sessionConfigEnvironment(environment): Pick<SessionConfigContext, "home" | "hostOwnsTrust">

Defined in: harness/core/session-config.ts:69

The context’s two process-wide parts, read from environment: the user’s ENSO_HOME, and whether the web host owns project trust (it sets ENSO_PROJECT_TRUST_BY_HOST before its first session).

Readonly<Record<string, string | undefined>>

Pick<SessionConfigContext, "home" | "hostOwnsTrust">


sessionEnsoConfig(session, context): Promise<SessionEnsoConfig>

Defined in: harness/core/session-config.ts:87

The session’s config, resolved once per session (the context’s sessionManager) and trust answer: under the web host a Trust reloads the session, and the trusted resolution is a new one.

object

SessionConfigContext

Promise<SessionEnsoConfig>

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>