Repository navigation
Rebrand as toolpass: secure-by-default agent tools, with permission checks built in - #64
Merged
Merged
Conversation
A decorator that wraps a plain Python tool function in a fixed order of checks: the user from the app's session, argument types and validators, scope limits, per-session action limits, authorization (hallpass or any callable), an untrusted-input check, an exfiltration guard for the lethal trifecta, out-of-band approval, credential injection, and one audit event per call. Untrusted output is fenced in a nonce-tagged block. The decorated function keeps its signature, so any agent framework's @tool can sit on top. Standalone package under securetools/, not part of the hallpass wheel. Includes a scripted ops-agent example and 122 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HefJaHn3tkhyyaMbyHup31
- Annotated *args and **kwargs are type-checked per item. - A limit slot is given back on any exit before the body, cancellation included; such calls are audited as "cancelled". - Any unexpected failure in a check (a validator, a scope predicate, the session source) refuses with check_error instead of escaping raw. - Approval keys are built from canonical, JSON-safe arguments, and include the reasons: an approval does not cover a new reason. - Untrusted output in dataclasses, pydantic models and other objects is recorded. - A body exception whose message carries the credential is re-raised as ToolError with the secret redacted. - Type hints resolve one at a time; an unresolvable one is logged and leaves only its own argument unchecked. - Callable objects with an async __call__ are async tools. - Cheap hooks (approval rules, previews, ApprovalQueue, a credentials mapping) run inline; authorizers, credential providers and custom approvers still run off the event loop. CI runs the prototype's lint, types and tests on Python 3.10 and 3.13, and once without optional packages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HefJaHn3tkhyyaMbyHup31
…al keys From the security review's below-the-bar notes: - Redaction covers bytes, sets, NamedTuples and any object whose text mentions the secret, and credentials that are pairs, lists or mappings of strings, in the output and in a body's exception. - Approval keys tag every container with its kind, so a list of pairs and a dict never share an approval. - describe() shows arguments with ASCII escapes, so bidi and invisible characters are visible to the person approving. - README: redaction matches literal text only; give arguments that hallpass_check interpolates a scope rule or validator. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HefJaHn3tkhyyaMbyHup31
…permission engine - securetools is now toolpass (package, imports, logger, CI job), 0.1.0 alpha; the hallpass extra is now [permissions]. - The top-level README is toolpass's page: what it stops (from the ops-agent example), what a tool can declare, the exfiltration guard, permission checks through hallpass, and how to install from this repository. - hallpass's former landing page moves to docs/permission-checks.md, links rewritten; the docs index points to it and to toolpass. The hallpass packages (Python, Node client, image, chart) and their names are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HefJaHn3tkhyyaMbyHup31
- Generator tools are refused at declaration: their body would run after the bookkeeping, unredacted and unrecorded. - A validator accepts only with a truthy answer; None (a failed re.fullmatch) now rejects. - ApprovalQueue: a listener that raises no longer strands the request; the other listeners still run and the retry asks again. - A failure after the body ran is audited as raised, not refused. - An untrusted_output tool's exception text is recorded as untrusted and fenced (raised as ToolError). - Previews shown to the approver escape control and bidi characters; argument keys that read alike are all shown. - Redaction walks dataclass fields (repr=False too) and pydantic models, through one string walker shared with the untrusted record. - An authorization_error shows only the exception type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HefJaHn3tkhyyaMbyHup31
A tool that returns a lazy iterator (a generator expression, iter(), map) handed it back untouched: the framework consumed it later, past credential redaction and the untrusted-output record. A returned iterator is now turned into a list first; an async iterator is refused. From the security review (below its reporting bar, closed anyway). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HefJaHn3tkhyyaMbyHup31
…ssion engine The repository no longer uses the old name anywhere: - hallpass-py is toolpass-py, and its package is toolpass. The toolkit (Toolkit, Session, ApprovalQueue, ...) moves into the same package next to the engine (Toolpass, guarded, the framework adapters, the server), so `pip install toolpass` is everything; hallpass_check is permission_check, and the [permissions] extra is gone. - hallpass-ts is toolpass-ts, published as toolpass-client. - The CLI is `toolpass`, the config file toolpass.yaml, environment variables TOOLPASS_*, the image ghcr.io/<owner>/toolpass, the Helm chart deploy/helm/toolpass. - Docs, examples, CI, release workflow, issue templates, skills and CLAUDE.md follow. RELEASING.md describes setting up trusted publishing for the new package names. - The unreferenced demo GIF, MP4 and flow image go; the social preview is redrawn for toolpass. Lockfiles regenerated with npm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HefJaHn3tkhyyaMbyHup31
- The release pushes the image to ghcr.io/<owner>/toolpass whatever the repository is called, matching the chart and the docs. - Scope globs: a "]" right after "[!" is a member of the class; and every reading of a value (percent-decoded, NFKC-folded) must stay in scope, so "%2e%2e", an encoded "/" and full-width dots cannot walk out. - name= reaches the wrapper's __name__, so the framework exposes the tool under the name the checks and the approval messages use. - Approvals are claimed, then spent only when the call reaches its body: an unavailable credential no longer costs the person another approval, and a concurrent retry cannot use the same approval twice. The key includes the preview, and objects are keyed by their state (every field) rather than their repr. - In async tools, sync hooks (approval rule, preview, approver and its listeners) run off the event loop; async hooks run on it. - permission_check checks its resource placeholders against the tool's parameters at declaration. - The untrusted-output record holds at most 100,000 digests by default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HefJaHn3tkhyyaMbyHup31
…rators - `secured_tool` is the decorator's name: module-level on a default toolkit that `toolpass.configure(...)` sets up (tools declared earlier read the settings at call time), and as a Toolkit method. `tools.tool` stays as the same method. - `as_tool=` takes any framework's tool decorator (LangChain's `tool`, the OpenAI Agents SDK's `function_tool`, Pydantic AI's `Tool`, CrewAI's `tool`) and returns that framework's tool with every check inside. Typing overloads keep the function's type without it. - Examples and docs use `guard` for the framework adapter objects, so no variable shadows the `toolpass` package. - READMEs and the example use `@secured_tool`; on_refuse=str is documented for frameworks that hide a tool's exception. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HefJaHn3tkhyyaMbyHup31
From the security review of the rename: keying objects by model_dump() dropped pydantic fields marked exclude=True (and anything a serializer rewrites), so approving Transfer(amount=5) also covered amount=999999. Approval keys now use the raw values of every dataclass field, pydantic field and extra, or __dict__; describe() shows the same values instead of a repr that may hide repr=False fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HefJaHn3tkhyyaMbyHup31
npm allows trusted publishing only for a package that already exists, so the first version is published by hand. Until then the npm job checks the registry (unauthenticated) and skips with a note in the run summary instead of failing the release; any registry answer other than 200 or 404 still fails it. RELEASING.md says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HefJaHn3tkhyyaMbyHup31
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The project becomes toolpass: one Python package with a new secure-tools toolkit and the existing permission engine. The old name is gone from the repository.
The toolkit.
@secured_tool(...)wraps a plain Python function in a fixed order of checks:permission_check, or any callable);ApprovalQueue), spent only when the call runs;Anything that cannot be evaluated refuses the call.
toolpass.configure(...)sets the shared settings.as_tool=takes any framework's own tool decorator (LangChain'stool, the OpenAI Agents SDK'sfunction_tool, Pydantic AI'sTool, CrewAI'stool) and returns that framework's tool, with the checks inside.as_tool=, the function keeps its signature, so a framework's@toolcan sit on top.examples/ops_agent.pyshows each check in a scripted session.The rename.
hallpass-py/toolpass-py/(packagetoolpass, toolkit and engine together;hallpass_checkis nowpermission_check)hallpass-ts/toolpass-ts/(npmtoolpass-client)toolpasstoolpass.yamlTOOLPASS_*ghcr.io/<owner>/toolpassdeploy/helm/toolpassThe docs, examples, CI, release workflow, issue templates, skills and CLAUDE.md all follow.
README.mdis toolpass's page; the old landing page is nowdocs/permission-checks.md. The unreferenced demo GIF, MP4 and flow image are removed, and the social preview is redrawn.Before merging (the next daily release would otherwise publish under names that aren't set up):
toolpass. The links, badges and install URL already point there.toolpass(seetoolpass-py/RELEASING.md), or setPUBLISH_PYPItofalse.PUBLISH_NPMtofalseuntiltoolpass-clienthas been published once by hand and has a trusted publisher.Why
Custom agent tools usually run with one service credential for everyone, read content that can carry injected instructions, and can change things. The permission engine answered one question, "may this user do this?". toolpass covers the rest under one name, for a relaunch.
Testing
ruff format --check src tests examplesandruff check src tests examplespass (intoolpass-py)mypy --strict src/toolpasspassespython -m pytest -qpasses: 3337 passed, 77 skipped (no Docker for the real-Vault tests locally)TOOLPASS_SPECS_DIR=... pytest tests/integrations tests/harness_tests tests/contract): 1750 passedtests/toolkit: all pass.as_tool=checked against LangChain, the OpenAI Agents SDK, Pydantic AI and CrewAItoolpass-ts:npm ci && npm test(3 pass);examples/agent-ts: 29 pass; lockfiles regenerated with npmhelm lint deploy/helm/toolpass --strict: cleandocker buildplus the CI smoke test (/healthz, then allow and deny from the demo connection): passpython3 test/docs/linkcheck.py: all links resolve/code-review main highpasses and the security reviews, every finding fixed with regression tests🤖 Generated with Claude Code
https://claude.ai/code/session_01HefJaHn3tkhyyaMbyHup31