nc://taint-sentry

v0.1.0Pinned
ThomSharktools/nodecode-taint-sentry: Stamps every tool result with its source, then refuses a tainted session's credential reads, instruction writes and egress. - a rewrite of YAN-XBT/hermes-sentry (MIT), credited to YAN-XBT313c111· 10 Nov1 commit
test1 filecell.lisp22.5 KBpolicy.lisp12.0 KBsanitize.lisp6.5 KBsources.lisp16.9 KBtaint.lisp7.4 KB

nodecode-taint-sentry

Taint Sentry: stamps every tool result with where it came from, then gates the dangerous half of a session that has read something a stranger wrote. A Nodecode rewrite of YAN-XBT/hermes-sentry by YAN-XBT.

A page an agent fetched is not allowed to give it orders. The original puts a wall around the tool path: every result is stamped with its author, content written by a third party is wrapped in untrusted markers and marks the session tainted, and while the taint is live a policy judges each call. This cell is the same wall at Nodecode's one interception point, the :TOOL hook, which sees both the call and its result.

  • Provenance, not content. A web fetch, a search, a browser, an MCP server, a screenshot, an image, or a file this session itself wrote while its content was untrusted marks the session. A novel injection phrasing still gets gated, because the turn that read the page is gated, whatever the page said. A plain local file read is the user's own material and does not mark anything.
  • Stamp. A marked result loses its invisible characters (zero-width, bidi overrides, tag characters), has its instruction-shaped lines named (instruction_override, secret_target, egress_instruction, ...), and is wrapped in <untrusted source="..." reason="..." id="..."> markers with a note that the block is data. A trusted result is not touched at all.
  • Gate. While the taint is live: reading a credential, writing a file the agent reads as instructions later, and running a file the session downloaded are refused; sending data to a host nobody allowlisted and writing outside the workspace are held for the operator. A rule that does not depend on taint -- a credential read and a send in one shell step, or an edit to this cell's own policy -- applies always. An untainted session is never gated.

This is a seatbelt, not a sandbox. The taint mark is reliable, because it comes from which tool produced the content; the credential, egress and write detectors are pattern matching over a call's arguments, so an obfuscated command, a payload assembled to avoid the patterns, and a write through a tool this cell does not read can get past them.

Get it

Tell the agent "add the taint-sentry cell", or run nodecode add nc://taint-sentry.

Set it up

No key, no service, no network: the cell uses cl-ppcre over the calls it sees and keeps its state in memory. To change what it does, edit the taint-sentry section of the shared config (or /settings):

Member Default What it does
mode enforce enforce gates a tainted session, monitor records only, off does nothing
taint_ttl_seconds 1800 how long a mark stays live
allow_hosts localhost, 127.0.0.1, ::1, 0.0.0.0 hosts a tainted session may send data to
allow_paths none paths the policy never gates
trusted_tools none tools whose result is never treated as untrusted
untrusted_tools none tools always treated as untrusted
disabled_rules none rule names to turn down to allow
"taint-sentry": {
  "mode": "enforce",
  "taint_ttl_seconds": 1800,
  "allow_hosts": ["localhost", "127.0.0.1"]
}

Use it

It works without being asked: fetch a page, and the rest of the turn is gated.

taint_status   {tool, command}   # what this session carries, and how a call is judged
taint_receipts {last}            # what was stamped, gated, held and cleared (never the text)
/taint-sentry                    # the same, from the prompt
/taint-sentry receipts 20
/taint-sentry policy             # the rules as they stand
/taint-sentry clear              # drop every mark
(help :taint-sentry)             # the manual, in one paragraph

/taint-sentry clear is the operator's escape hatch: the taint is held in process memory, so clearing it is the one way back to an ungated session without a relaunch.

Gaps

The original writes a receipts ledger to disk (receipts.db) and has a CLI with a dry-run policy tester, a desktop pane and a dashboard route. This cell keeps its receipts in a ring of the session's newest 200, answers the same questions through taint_receipts and /taint-sentry, and has no durable ledger and no pane. Its pre_llm_call banner has no point to ride here: the envelope carries the same words into the conversation, and (help :taint-sentry) into every request's manual. The obfuscated-payload decoding (powershell -enc) is not carried.

Testing

From the cells repository: .github/test-cell.sh tools/nodecode-taint-sentry. The suite runs offline and needs nothing installed.

Add nc://taint-sentry

  1. 1

    Install Nodecode

    Once per machine.

    $ curl -fsSL https://nodecode.ai/install.sh | sh
  2. 2

    Add the cell

    Pinned at 313c111.

    $ nodecode add nc://taint-sentry

    Or tell the agent add the taint sentry cell.