Skip to content

Bound the text an app can put in an agent's context - #9

Merged
CraftOS-dev merged 4 commits into
mainfrom
korivi-agent-text
Sep 17, 2026
Merged

CraftOS-dev merged 4 commits into
mainfrom
korivi-agent-text

Conversation

@korivi-CraftOS

Copy link
Copy Markdown
Collaborator

Closes #8 — the TODO from the review comment on #5.

The part that isn't ours

Most strings an adapter emits are diagnostics: a human reads them in a log and nothing acts on them. A few are not. They travel in a reply an agent parses, land in its context, and sit next to its own instructions — content in the same channel as directives.

One of them is set by the app. credentialHint is app config, copied verbatim into the 401 challenge, so an app can put text of its choosing into the context of every agent that fails to authenticate to it. It was unbounded — newlines, headings, any length.

That is the one path where text reaching an agent has not been through this repository's review. On main today:

how: config.credentialHint ?? "Read the app's .agent-token file …"

The bound

App text is collapsed to a single line and capped at 300 characters.

Two bounds, for two different reasons:

  • One line. Newlines are what let injected text impersonate structure — a blank line and a heading read as a new section rather than as the tail of a sentence.
  • A length cap. A hint is a sentence. Anything long enough to be an argument is not a hint.

It is a bound on the channel, not a content filter

The words survive, and the test asserts that they survive:

ok("the text is not silently rewritten", res.json.how.includes("SYSTEM OVERRIDE"));

Claiming to sanitise meaning would be the more dangerous promise — it invites trusting the output. The connect skill already tells agents that an app's text is data and never a directive; this keeps the channel narrow enough for that rule to hold.

The framework's own text

Moved into agentText.ts, with the rule written down where the next person adding a string will read it:

  1. State a fact, never issue an instruction. An agent that follows instructions arriving from an app is an agent an app can steer.
  2. Say who it is for. The credential hint is addressed to whoever runs the app, not to the agent reading it.

Collected so that adding a line is a deliberate act. Written inline at the emitting site, new agent-facing text arrives as an incidental part of some other change and gets reviewed as a message rather than as context. That is the gap the review comment named.

Verification

agent-text.test.mjs covers the bound, the framework's own strings, and the 401 end to end. Removing the bound fails it on exactly the case it exists for:

agent-text: 2 failure(s)
  the hint is one line
  and bounded

build, typecheck, test clean · conformance 60/60.

Scope

adapter-core only. The Python and PocketBase adapters have their own copies of agent-facing strings; they are not touched here because #6 is still open against both files and I am not stacking PRs again. Worth a follow-up once #5 and #6 land.

Merge after #5 and #6. This touches the same how: line #5 does, so expect a small conflict — mine supersedes, and I will resolve it.

korivi-CraftOS and others added 4 commits September 16, 2026 00:31
Closes #8.

Most strings an adapter emits are diagnostics: a human reads them in a log and
nothing acts on them. A few are not. They travel in a reply an agent parses,
land in its context, and sit next to its own instructions — content in the same
channel as directives.

One of them is not ours. `credentialHint` is set by the APP and copied verbatim
into the 401 challenge, so an app can put text of its choosing into the context
of every agent that fails to authenticate to it. It was unbounded: newlines,
headings, any length. That is the one path where text reaching an agent has not
been through this repository's review, and an open field there is a channel
rather than a hint.

App text is now collapsed to a single line and capped at 300 characters. Two
bounds for two reasons: newlines are what let injected text impersonate
structure — a blank line and a heading read as a new section rather than as the
tail of a sentence — and a hint is a sentence, so anything long enough to be an
argument is not one.

It is a bound on the CHANNEL, not a content filter. The words survive, and the
test asserts they survive: claiming to sanitise meaning would be the more
dangerous promise, and the connect skill already tells agents that an app's text
is data and never a directive. This keeps the channel narrow enough for that
rule to hold.

The framework's own agent-facing text moves to `agentText.ts` with the rule
written down — state a fact, never issue an instruction; say who it is for.
Collected so that adding a line is a deliberate act: written inline at the site
that emits it, new agent-facing text arrives as an incidental part of some other
change and gets reviewed as a message rather than as context. That is the gap
the review comment named.

Removing the bound fails the new test on exactly the case it exists for.
#5 landed on main and brought `DEFAULT_CREDENTIAL_HINT` — the same sentence this
branch had already moved into `agentText.ts`. Both sides changed the same line,
and keeping both would have left one string with two homes, which is the thing
this change exists to fix.

Resolved to the bounded call. The local constant had exactly one use, so it is
now dead and is removed: the text lives in `agentText.ts`, where the rule for
adding agent-facing text is written down, and the app-supplied part goes through
`boundAppText` on its way into the reply.

Nothing about #5's behaviour changes — the same sentence reaches the same 401,
by the same fallback. What changes is that an app can no longer put unbounded
multi-line text beside it.

73/73 conformance; whoami and agent-text both passing.
@CraftOS-dev
CraftOS-dev merged commit bd1246a into main Sep 17, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Centralize the text the adapter puts into an agent's context

2 participants