Prism Human+ for Python
Humans and agents sharing one application surface, across a trust boundary. The
Python port of particle-academy/prism-human-plus.
Zero runtime dependencies. Python 3.10+.
pip install prism-ai-human-plus
An agent joins a running application surface, such as a spreadsheet a person has open, through the invitation that surface issued. It sees the surface's tools, calls the ones it is trusted with, and announces what it is doing to the people watching.
from prism_human_plus import (
HumanPlusManager,
HumanPlusToolset,
InMemoryAttachmentStore,
Participant,
SsePostRelayTransport,
SurfaceInvitation,
TrustPolicy,
)
transport = SsePostRelayTransport(
my_http,
allowed_relay_hosts=["relay.example.com"],
egress_proxy="http://egress-proxy.internal:3128",
)
human_plus = HumanPlusManager(
transport,
InMemoryAttachmentStore(),
TrustPolicy.allowing(["sheet_read", "sheet_propose_edit"]),
)
attachment = human_plus.attach(
session,
SurfaceInvitation(
relay_base_url="https://relay.example.com",
session_id=invitation["session_id"],
token=invitation["token"],
surface_id=invitation["surface_id"],
application="sheets",
),
Participant(id="agent-1", name="Assistant", color="#7c3aed"),
)
tools = HumanPlusToolset(human_plus).for_attachment(
session, attachment.id, approval_tools=["sheet_propose_edit"]
)
my_http is your HTTP client, behind the RelayHttp protocol (post and
stream). session is the owner: a string, or anything with a key() method,
such as a prism-ai-harness session.
Trust
TrustPolicy decides which of the surface's tools the agent may see and call.
TrustPolicy.undeclared()refuses discovery itself: no request of any kind reaches the surface, so no tool description reaches the model.TrustPolicy.allowing([...])allows the named tools.TrustPolicy.every_tool()allows every tool the surface offers.- Pins. Pass
pins={name: digest}to any of them. A pin isToolDefinition.digest(), a fingerprint of the tool's name, description and schema. A tool whose definition no longer matches its pin is refused. - Confirmation tools belong to the human. A tool named
confirm,reject,accept,approveordeny, or ending in_confirm,_rejectand so on, is refused under every policy, includingevery_tool(). The name is normalised first, so a trailing space or an invisible character does not get around it. - A malformed tool name is refused.
Every refusal raises ToolRefused.
Approval is your decision, not the surface's. HumanPlusToolset marks a
tool requires_approval only if you list it in approval_tools. The surface's
own annotations are not consulted.
Results
Every tool result passes through a ResultGuard:
- A result over the byte budget (65,536 by default) is refused, not truncated.
- The text is wrapped in an
<untrusted-tool-output>tag with a random id, so the surface's output cannot close the wrapper.
The wrapper makes a prompt injection harder. It does not make one impossible.
Attachments
- An attachment id locates state; it is not a credential. Every call re-presents
the owner, and an attachment belonging to another owner raises
AttachmentUnauthorized. SurfaceInvitationis validated when built: an HTTPS relay URL, a well-formed session id and a token of at least 16 characters.- A surface that has gone (
410 session_gone) raisesSurfaceUnavailable. One that was never authorized (401) raisesAttachmentUnauthorized. Each is recorded as its own terminal state, and neither is retried. AttachmentStorewrites carry a generation, so two workers cannot overwrite each other's state.InMemoryAttachmentStoreis for tests and single processes.
Every failure subclasses HumanPlusError.
The relay transport
SsePostRelayTransport speaks the Fancy relay: a POST, then a bounded
server-sent-events stream.
- The relay URL is checked on every call against
allowed_relay_hostsandallowed_relay_ports(443 by default). A literal private address is refused. - It refuses to run without an
egress_proxyunless you passallow_unverified_egress=True, which is for isolated local testing only. - Declaring
egress_proxydoes not route traffic by itself. YourRelayHttpmust send its requests through it. Host names are not resolved here, so the proxy is what stops a public name that resolves to a private address. - A frame over
max_frame_bytes(262,144 by default) is refused.
Two writers, and what changed since my last turn
A human editing the same surface as the agent used to lose their work in silence. The agent read, the person committed, the agent wrote, and both writes succeeded — which is exactly what a lost update looks like from the inside.
Two mechanisms, and they answer different questions.
The revision: did the world move under me
A tool result may carry a marker for the state it just showed. It is stored on
the attachment and pinned to every later call — reads included, because this
package cannot tell a read from a write and MCP's readOnlyHint is explicitly a
hint the spec says not to trust for security decisions.
If the surface says the marker is stale the call is refused with
SurfaceChangedUnderYou, nothing is written, and the stored marker is
dropped so the agent can read again.
conflict_detection() reports what has been OBSERVED, which is less than what
is configured:
ConflictDetection |
What is known |
|---|---|
NOT_OBSERVED |
The surface has not answered yet. |
UNAVAILABLE |
It mints nothing. A concurrent edit will be lost silently. |
MINTED |
It mints, so every call is pinned. Whether it enforces is not observable. |
ENFORCED |
It refused a stale pin. Proven, because it happened. |
MINTED is the one to read carefully. The reference's first integrator minted
on every write result and read an incoming pin nowhere, so a pinned call was
applied exactly as an unpinned one — and the boolean this replaced said True.
The change feed: what did somebody else do
A revision stops an agent overwriting a change it did not know about. It does nothing about an agent that re-reads, sees current state, decides the surface has drifted from what it intended, and puts it back — over a person's edit, with nothing stale anywhere and no error at any layer.
changes = human_plus.changes_since(owner, attachment_id)
if not changes.answered():
... # the surface has no feed; an empty list is NOT evidence of quiet
for change in changes.defer_to():
change.handle # the surface's own id
change.kind # CREATED | UPDATED | DELETED | MOVED | UNKNOWN
change.actor # HUMAN | AGENT | OTHER | UNKNOWN
The empty list is the dangerous value. "Nothing changed since your marker"
and "I cannot answer that question" are the same empty list on the wire, so
ChangeFeed comes first and nothing_changed() is the only method that means
what an empty list looks like it means.
Attribution is the load-bearing field. A change is deferred to unless the surface positively said this agent made it. On a surface where every write path is an agent tool — the first one asked is exactly that — nothing is attributed, so everything is deferred to: not because it is all a person's, but because none of it can be shown to be the agent's own.
complete is true unless the surface says otherwise. It exists because feeds
have holes their authors know about: the first surface asked hard-deletes rows
with no tombstone, so a removal moves no revision and appears in no feed.
See the reference's README for the surface's half of the contract — the tool
names, the key names read per field, and why change must be sent even when
kind is.
Parity
prism-parity's human-plus-tool-admission corpus compares tool admission with
the PHP reference and the TypeScript port. Admission and the confirmation-tool
reservation agree on every case.
Two pins differ. A tool with no schema gets a different digest in the PHP
reference, and a schema containing an integral float such as 1.0 gets a
different digest here. Compute a pin in the language that checks it.
License
MIT. See LICENSE.
Metadata
Release files for prism-ai-human-plus 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| prism_ai_human_plus-0.1.0.tar.gz | 27.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| prism_ai_human_plus-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 56.1 kB
Release files / prism_ai_human_plus-0.1.0.tar.gz
| Download URL | prism_ai_human_plus-0.1.0.tar.gz |
|---|---|
| Size | 27.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
88b61979f24b2deca1a0727085ee7d8bad13da6f0ca75e45ee8aebe6de65346c
|
|
BLAKE2b-256 checksum How to use checksums |
d91af2dad8b1d7cd3fe39ef64a9eca4e8aac61c7c1acd5ad8639b5ebd3e539ce
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.
Transparency logRelease files / prism_ai_human_plus-0.1.0-py3-none-any.whl
| Download URL | prism_ai_human_plus-0.1.0-py3-none-any.whl |
|---|---|
| Size | 28.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0a6d4e463d423cee3a5ef077cd83ddd10a00d5a0ad48b442cc90508605108231
|
|
BLAKE2b-256 checksum How to use checksums |
ba7eee58672e39622c55be97b98c814a929e58268d7e4bfd9dc6113d3b8c8125
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.
Transparency log