Skip to content
Merged
25 changes: 24 additions & 1 deletion reference/configuration/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,30 @@ Whatever `componentsScope` and `configScope` say, every scope refuses:

A key with neither a key file name nor PEM armor, such as a `.p12` bundle or raw DER, is not recognized outside the key directories.

The config file can still hold secrets that are not PEM keys, such as a model `apiKey` or storage credentials, and the agent reads it. Setting `configScope` to `rootPath` also restores read access to the raw database files and backups.
Setting `configScope` to `rootPath` also exposes the rest of `rootPath`, though a file over 256 KiB or not UTF-8, such as a database file or a backup, is refused (see [Masked credential values](#masked-credential-values)).

### Masked credential values

<VersionBadge type="changed" version="v5.4.0" />

The config file can hold plaintext credentials: a model `apiKey` passed through `HARPER_SET_CONFIG` is written there. Logs hold whatever a component logged. When the agent reads the `logs` or `config` scope, the value of every credential-named key comes back as `*`s:

```yaml
models:
generative:
default:
apiKey: **************
model: gpt-5
```

- **Which keys.** A key that ends in `key`, `token`, `cookie`, or `authorization`, or contains `secret`, `password`, `passwd`, `passphrase`, or `credential`, and `accessKeyId` in any spelling. The whole key must match, so settings such as `maxTokens` and `operationTokenTimeout` are shown. A value that is exactly an environment reference, such as `${OPENAI_API_KEY}`, is shown as written.
- **Which forms.** `key: value` and `key=value`, with the key quoted or not, anywhere in a line: YAML, JSON (including JSON logged inside a JSON string), `Authorization: Bearer ...` headers, and `?api_key=...` query strings. A quoted value is masked up to its closing quote, even on a later line, and an unquoted one to the end of the line. A YAML value that continues on more-indented lines, such as a long folded value or a `|` block, is masked on every line, as are the `- ` items under a key with no value of its own. In the `config` scope each file is also parsed as YAML, so a key written with escapes, such as `"api\u004Bey"`, and a value reached through an alias are masked too. A config file that does not parse shows only its keys, with every value masked. A config file over 256 KiB or not UTF-8 is refused.
- **Positional reads.** The mask keeps each value in place (in a config file, one `*` per byte), so a `read_file` `offset` into a value, the later parts of a long line, and the end of a line that `tail_file` returns are masked too. `size` and `nextOffset` still count the file's bytes, so a masked value's length is visible; its content is not.
- **Searches.** `grep_files` searches the masked text, so a pattern that matches only a credential's value finds nothing.

The agent's `read_log` tool is masked the same way, in every text field of each entry. An entry holding a PEM private key is replaced by a refusal. The tool refuses `read_log`'s `filter` argument, because `filter` matches the unmasked log. `read_log` called through the operations API or an MCP client returns the log unmasked.

The `components` scope is not masked, so a file the agent reads and writes back is unchanged. A credential that is not the value of a credential-named key, such as a password inside a URL, is not masked.

### Restricting `http_fetch`

Expand Down
4 changes: 4 additions & 0 deletions release-notes/v5-lincoln/5.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ A model reply that did not finish now ends the agent session `error`, with the r

The built-in agent's read-only `config` filesystem scope used to be the directory holding `harper-config.yaml`. On a default install that is `rootPath`, so the agent could read `keys/`, `ssh/`, and the raw database files. It is now the config file alone. The new `agent.configScope` replaces it at startup with another file or directory, which keeps the config file readable only if it contains it. In every scope, the agent's filesystem tools now refuse to read Harper's key directories (`<rootPath>/keys` and `<rootPath>/ssh`), files named `*.pem`, `*.key`, or `.jwtPass`, and text holding a PEM private key, and they refuse to write into the key directories. `set_agent_config` now rejects `configScope` and `componentsScope` with a `400`, as it already did `httpFetch`. An agent that read other files through `config` needs `configScope` set to a directory that holds them. See [Filesystem scopes](/reference/v5/configuration/options#filesystem-scopes) ([harper#3041](https://github.com/HarperFast/harper/issues/3041)).

### Credential Values Are Masked in the Agent's Config and Log Reads

The config file can hold plaintext credentials, such as a model `apiKey` passed through `HARPER_SET_CONFIG`, and the agent's `config` scope returned them, as did its `logs` scope and `read_log` for anything a component logged. The agent's reads of the `logs` and `config` scopes, and its `read_log` results, now show the value of every credential-named key (`apiKey`, `password`, `token`, `secret`, and similar) as `*`s, including a value that continues on more-indented YAML lines and a value a positional read starts in the middle of. `grep_files` searches the masked text in those scopes, and the agent's `read_log` refuses `filter`, which matches the unmasked log. The `components` scope, and `read_log` called through the operations API or an MCP client, are unchanged. Sessions recorded before this release keep whatever they already read, so rotate any credential an agent session may have read. See [Masked credential values](/reference/v5/configuration/options#masked-credential-values) ([harper#3097](https://github.com/HarperFast/harper/issues/3097)).

### Capped and Paged Agent Tool Results

The built-in agent added every tool result to its conversation in full, so one large result, such as a log read whole, could fill the model's context window and leave the session unusable. A tool result is now cut to `agent.maxToolResultBytes` (default `65536`) before it is stored, and `read_file` pages through a file by line, with no size limit, returning a cursor (`nextLine`, `nextOffset`) to continue from. When a provider still rejects a request as too long for the model, the agent cuts the most recent tool results to 2 KiB and retries once, and prompting a session left unusable by an earlier version usually recovers it the same way. The agent also stops sending its system prompt twice per request. See [When a conversation outgrows the model's context window](/reference/v5/operations-api/operations#when-a-conversation-outgrows-the-models-context-window).
Expand Down
Loading