Skip to content

Rebrand as toolpass: secure-by-default agent tools, with permission checks built in - #64

Merged
roee-hersh merged 11 commits into
mainfrom
ccr-14582a82-6py9dc
Oct 2, 2026
Merged

roee-hersh merged 11 commits into
mainfrom
ccr-14582a82-6py9dc

Conversation

@roee-hersh

@roee-hersh roee-hersh commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

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:

  1. the user from the app's session, never from the model;
  2. argument types and validators;
  3. scope limits;
  4. per-session action limits;
  5. permission checks (permission_check, or any callable);
  6. an untrusted-input check;
  7. an exfiltration guard for the lethal trifecta;
  8. out-of-band approval (ApprovalQueue), spent only when the call runs;
  9. credential injection and redaction;
  10. fencing of untrusted output, and one audit event per call.

Anything that cannot be evaluated refuses the call.

  • toolpass.configure(...) sets the shared settings.
  • as_tool= takes any framework's own 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 the checks inside.
  • Without as_tool=, the function keeps its signature, so a framework's @tool can sit on top.
  • examples/ops_agent.py shows each check in a scripted session.

The rename.

Before After
hallpass-py/ toolpass-py/ (package toolpass, toolkit and engine together; hallpass_check is now permission_check)
hallpass-ts/ toolpass-ts/ (npm toolpass-client)
CLI toolpass
Config file toolpass.yaml
Environment variables TOOLPASS_*
Container image ghcr.io/<owner>/toolpass
Helm chart deploy/helm/toolpass

The docs, examples, CI, release workflow, issue templates, skills and CLAUDE.md all follow. README.md is toolpass's page; the old landing page is now docs/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):

  1. Rename the GitHub repository to toolpass. The links, badges and install URL already point there.
  2. Add a PyPI pending publisher for toolpass (see toolpass-py/RELEASING.md), or set PUBLISH_PYPI to false.
  3. Set PUBLISH_NPM to false until toolpass-client has 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 examples and ruff check src tests examples pass (in toolpass-py)
  • mypy --strict src/toolpass passes
  • python -m pytest -q passes: 3337 passed, 77 skipped (no Docker for the real-Vault tests locally)
  • Spec-validated suites (TOOLPASS_SPECS_DIR=... pytest tests/integrations tests/harness_tests tests/contract): 1750 passed
  • Each of the nine framework adapters with its extra installed, plus tests/toolkit: all pass. as_tool= checked against LangChain, the OpenAI Agents SDK, Pydantic AI and CrewAI
  • The installed wheel, tested from outside the repository: 3313 passed
  • toolpass-ts: npm ci && npm test (3 pass); examples/agent-ts: 29 pass; lockfiles regenerated with npm
  • helm lint deploy/helm/toolpass --strict: clean
  • docker build plus the CI smoke test (/healthz, then allow and deny from the demo connection): pass
  • python3 test/docs/linkcheck.py: all links resolve
  • Three /code-review main high passes and the security reviews, every finding fixed with regression tests
  • CI green on the head commit

🤖 Generated with Claude Code

https://claude.ai/code/session_01HefJaHn3tkhyyaMbyHup31

claude added 8 commits October 2, 2026 16:54
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
@roee-hersh roee-hersh changed the title toolpass: secure-by-default agent tools, with hallpass as its permission engine Rebrand as toolpass: secure-by-default agent tools, with permission checks built in Oct 2, 2026
claude added 3 commits October 2, 2026 18:33
…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
@roee-hersh
roee-hersh merged commit 2b931ce into main Oct 2, 2026
22 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.

2 participants