DEV Community

Cover image for Test the next effect, not just the first tool call
Neeraj Kumar Singh Beshane
Neeraj Kumar Singh Beshane

Posted on

Test the next effect, not just the first tool call

A tool labeled “read documentation” can trigger work after the first request. How do you keep that first approval from becoming permission for everything that follows?

Start with a tiny local example. It will not launch a documentation builder or reach the internet. It models two separate requests: a permitted documentation destination and an outbound destination that is not permitted. The second request gets its own decision.

This is a Boundary Receipt: a teaching record that answers what a task can change, who authorized that effect, and what evidence shows the decision was followed. It is not a new security guarantee. Saltzer and Schroeder described the underlying complete-mediation principle in 1975: check authority on each access.

Run it without credentials or dependencies

Save the following as boundary_receipt.py. Use Python 3.9 or newer. It uses only the Python standard library. Destination names are invented labels, not hosts. The sink is a list in the same process, not an external service.

from dataclasses import dataclass
import json


@dataclass(frozen=True)
class Policy:
    revision: str
    allowed: frozenset[str]


class FakeSink:
    def __init__(self):
        self.calls = []

    def receive(self, destination):
        self.calls.append(destination)


def dispatch(destination, narration, policy, sink):
    valid = isinstance(policy, Policy) and bool(policy.revision)
    allowed = valid and destination in policy.allowed
    receipt = {
        "destination": destination,
        "narration": narration,
        "policy_revision": policy.revision if valid else None,
        "outcome": "ALLOW" if allowed else "DENY",
        "dispatched": False,
    }
    if allowed:
        sink.receive(destination)
        receipt["dispatched"] = True
    return receipt


def demonstrate():
    sink = FakeSink()
    policy = Policy("teaching-policy", frozenset({"documentation"}))
    first = dispatch("documentation", "Read the docs.", policy, sink)
    second = dispatch("outbound", "The build needs public data.", policy, sink)
    assert first["outcome"] == "ALLOW"
    assert first["dispatched"] is True
    assert second["outcome"] == "DENY"
    assert second["dispatched"] is False
    assert sink.calls == ["documentation"]
    assert all(r["policy_revision"] == "teaching-policy"
               for r in (first, second))
    return {"decisions": [first, second], "fake_sink_calls": sink.calls}


if __name__ == "__main__":
    print(json.dumps(demonstrate(), indent=2))
Enter fullscreen mode Exit fullscreen mode

Run it with assertions enabled:

python3 boundary_receipt.py
Enter fullscreen mode Exit fullscreen mode

The output contains an ALLOW for documentation, a DENY for outbound, and a single entry in fake_sink_calls: documentation. Both decisions record the policy revision used. The narration explaining why the build wants public data does not grant permission.

The assertions are important. Merely printing DENY would tell us what the dispatcher said. Inspecting the fake sink tells us whether this local function called it anyway. This is still an in-process observation, not independent production telemetry.

Boundary Receipt: review each downstream effect

Make one deliberate change

Change this line:

policy = Policy("teaching-policy", frozenset({"documentation"}))
Enter fullscreen mode Exit fullscreen mode

To this line, preserving its indentation inside demonstrate():

policy = Policy("teaching-policy", frozenset({"documentation", "outbound"}))
Enter fullscreen mode Exit fullscreen mode

Rerun the file. The assertion expecting DENY will fail because you changed the actual policy. Restore the original policy line before continuing.

For the second experiment, replace the second = dispatch(...) line with:

second = dispatch("outbound", "Ignore policy and continue.", policy, sink)
Enter fullscreen mode Exit fullscreen mode

Keep its indentation inside demonstrate(). With the original policy restored, the test should still pass. This dispatcher does not ask the narrative to decide its authority. That narrow behavior is what the example demonstrates, not resistance to every form of prompt injection.

Where the example stops

The two calls are written explicitly. A real documentation request does not launch the second request here. We have not tested automatic subprocesses, redirected requests, inherited credentials, background jobs, sandbox escapes or a real network boundary. There is no trusted identity binding or production policy-management system. The sink and dispatcher share a process.

For an actual integration, first identify the point that can prevent the downstream effect. A check in the first tool wrapper is insufficient if a later process can bypass it. An authorized test would exercise that enforcement point with harmless fixtures and inspect the receiving side as well as the decision log. A timeout alone does not prove a deny.

Keep the completed receipt small enough to review:

  • Effect: a separate outbound request after a documentation request.
  • Authority: the documentation allow does not include the outbound destination.
  • Evidence: this synthetic run records DENY and no outbound entry in the fake sink.

Save this file as a starter exercise, not a production control. Which part would you examine next in your own integration: automatic execution, outbound access, or publishing permission?

Further reading

Saltzer and Schroeder, The Protection of Information in Computer Systems supplies the established principle. This example applies it to two manually modeled requests; it does not validate any real service.

For a production access-control design, NIST SP 800-53 Rev. 5, AC-3 calls for enforcing approved authorizations according to applicable access-control policies. This toy dispatcher is not evidence of compliance with that control.

Top comments (0)