Deterministic capability-selection gate: select one safe, evidence-bound capability path or fail closed.
Project description
ChoiceGate
ChoiceGate decides how an AI agent should get a task done. It weighs every candidate capability (skills, local tools, MCP servers, integrations), and not just what is already installed: when something materially better exists it surfaces that as an owner-approved install choice, and it treats driving a browser or doing the work by hand as the explicit last resort. Every decision returns a deterministic receipt recording what was chosen, why, and what it does not permit; on malformed or ambiguous input it refuses with a named error, and it never installs, configures, or runs what it selects.
Getting started
Prerequisites: Python 3.11 or newer (standard library only, no third-party packages) to run the command-line interface, and Claude Code to use ChoiceGate as a plugin.
Install: two labeled paths, pick the one you need.
- In Claude Code (one command): run
/plugin marketplace add https://gitlab.com/krahul02004/ChoiceGateand then/plugin install choicegate. This is a self-hosted marketplace served from this repository (the root-level.claude-plugin/marketplace.json), and it is not published to any external or third-party plugin registry. - Python CLI:
pip install choicegate. This installs three console entry points (choicegate-dispatch,choicegate-route, andchoicegate-rank) and needs nothing beyond the Python standard library.
Check it works: after the CLI install, run one command and confirm the usage line it prints.
choicegate-dispatch --help
usage: choicegate-dispatch [-h] request
Why the gate exists
Capability selection is a policy decision, not a search-results list. An agent that grabs the first
plausible match will happily pick a tool that is not installed, not authorized, in conflict with
another owner, or described by stale information. ChoiceGate checks eligibility, authorization,
host compatibility, evidence freshness, and cost, breaks ties deterministically, and returns
exactly one path. When those checks cannot all be satisfied it fails closed: you get a
no-safe-route receipt naming the reason, never a best guess. A wrong refusal costs a retry; a
wrong selection runs the wrong thing.
flowchart LR
A["Typed request"] --> B["Frozen candidate evidence"]
B --> C{"Exactly one eligible owner?"}
C -- "No" --> D["Fail closed / no safe route"]
C -- "Yes" --> E["Deterministic decision receipt"]
E --> F{"Separate execution approval?"}
F -- "No" --> G["Stop"]
F -- "Later" --> H["Selected workflow executes elsewhere"]
Beyond the installed stack
The candidate pool is not limited to what is already installed. Discovery walks a bounded tier
order (the current session's callable tools and skills, the client's plugin marketplace, the
official MCP registry, provider documentation, then narrow public-web corroboration; one focused
pass per tier), and every candidate carries one of five statuses that the ranker weighs directly:
installed_usable outranks requires_auth_or_config, which outranks available_to_install, while
unverified_or_risky and browser_or_manual_fallback contribute nothing to the score. A
not-yet-installed candidate must also clear a material-improvement gate before it can rank at all:
strong task fit, a credible advantage over doing the job by hand, and a weighted score of at least
65, or it is rejected with the reason recorded. Winners that still need installation or
configuration are never acted on; the router places them in a setup-required pool whose
disposition is requires-owner-setup-approval, and only an explicit owner-approved setup path can
move them further. Driving a browser or doing the work manually stays on the list as the explicit
last resort, offered only as an owner choice when no trustworthy integration is materially better.
And unknown is not neutral: a candidate whose security, privacy, or provenance cannot be verified
scores zero on those dimensions and is rejected, not guessed about.
Try it
Thirty seconds, standard library only, from a bare checkout (Python 3.11+). The repository bundles
a sample request, evals/choicegate/sample-family-request.json:
{
"contract_version": "choicegate.family-request/v1",
"request_id": "readme-try-it",
"claims": [
{"domain": "tools", "specificity": "explicit", "evidence_id": "owner-asked-for-a-local-cli"}
],
"legacy_invocation": false
}
Feed it to the dispatcher (pass - to read stdin instead; after pip install choicegate the same
command is choicegate-dispatch):
python -B scripts/select_family_leaf.py evals/choicegate/sample-family-request.json | python -m json.tool
Actual output (the claims echo is trimmed; everything else is verbatim):
{
"claims": [...],
"decision": {
"leaf_id": "choicegate-tools",
"reason": "single-domain-claim",
"route_type": "atomic-leaf"
},
"prohibited_actions": [
"activate",
"authorize",
"configure",
"enable",
"execute-selected-capability",
"expose",
"install",
"promote"
],
"receipt_sha256": "e9c95a6a5f06248e59e8142ddcbc10e3531fb699580ca8bca3a5f3c8a98a3b7a",
"receipt_version": "choicegate.family-dispatch-receipt/v1",
"request_id": "readme-try-it",
"request_sha256": "790416834f601a3c945bd058fbb3bbd76e9a333fde18ee92fdef77f74b89bdba"
}
One eligible owner, one receipt, request and receipt pinned by SHA-256, and an explicit list of
actions the selection does not authorize. Add a second "explicit" claim to the request and the
receipt becomes no-safe-route / ambiguous-domain-claims: the gate refuses rather than guessing
between two owners.
choicegate-rank needs no registry either: it scores caller-supplied candidates directly. A second
bundled request, evals/choicegate/sample-rank-request.json, describes one candidate. Two gotchas:
candidates[].status must be one of installed_usable, requires_auth_or_config,
available_to_install, unverified_or_risky, or browser_or_manual_fallback, and all twelve
ratings keys are required; a missing key is reported as "must be a number from 0 to 5".
python -B scripts/rank_candidates.py evals/choicegate/sample-rank-request.json
Actual output, trimmed:
{
"duplicates": [],
"fallback": null,
"not_selected": [],
"owner_choice_required": true,
"ranked": [
{
"accepted": true,
...
"score": 95.7,
"source_url": "https://github.com/burntsushi/ripgrep",
"status": "installed_usable"
}
],
...
}
Exercise the full router
The bundled sample-route-request.json embeds a 16-component synthetic registry profile and one
public-data candidate. It exercises the same inventory hashing, schema, lifecycle, exclusion,
ranking, and receipt path as an owner registry without publishing or pretending to reproduce the
owner's private snapshot:
$choicegateCommit = git rev-parse HEAD
python -B scripts/route_capabilities.py evals/choicegate/sample-route-request.json `
--choicegate-commit $choicegateCommit | python -m json.tool
The receipt selects demo-local-formatter as an executable atomic route, identifies
registry_profile as public-demo-v1, and records accepted_registry_commit as null because the
synthetic profile makes no Git provenance claim. Change any embedded component byte or use the demo
profile outside its fixed fixture scope and the router fails closed. Selection still does not run
the formatter or authorize any outward action.
How it works
ChoiceGate is a family of seven small skills sharing one routing core. The first is a dispatcher that routes a request to whichever of the other six owns it; each of those six does exactly one job:
| Skill | Its one job |
|---|---|
choicegate |
Route a request to the one skill below that owns it, or return no safe route. |
choicegate-skills |
Pick one eligible skill, or one approved skill bundle. |
choicegate-tools |
Pick one local command-line tool or deterministic script. |
choicegate-integrations |
Pick one integration (API, plugin, MCP server, connector), or a receipt saying its setup needs owner approval. |
choicegate-generators |
Pick one model or media generation capability, without calling it. |
choicegate-context |
Pick one way to spend a stated context budget. |
choicegate-refresh |
Decide whether an earlier decision is still valid or must be redone. |
Two programs implement the family. scripts/select_family_leaf.py (choicegate-dispatch) is the
dispatcher you ran above; it needs nothing but the request. scripts/route_capabilities.py
(choicegate-route) is the full router: it takes frozen candidate evidence and judges it against a
capability registry (a separate, owner-maintained catalog of which capabilities exist and whether
they are installed, enabled, and authorized) that ChoiceGate pins by content hash so a decision
can never rest on a catalog that has silently changed. The owner snapshot itself is not in this
repository. The separately pinned synthetic profile above makes the complete router path publicly
reproducible without claiming owner authority. Strict JSON
(duplicate keys rejected), exact schemas, and canonical hashing apply to both programs, so every
receipt is reproducible byte for byte.
The demo uses the real local test result and contains no owner path, credential, remote claim, or fabricated usage metric.
Verify it yourself
The offline suite covers the dispatcher, all seven owners, trigger and near-miss fixtures, pairwise domain collisions, strict JSON duplicate-key rejection, receipt hashing, adapter hash bindings, packaging metadata, and deterministic package contents:
python -B -m unittest discover -s tests -p "test_*.py"
python -B -m compileall -q scripts tools
A stronger maintainer suite additionally validates the router against the exact owner snapshot pinned in
references/routing-contract.md; see docs/public/VALIDATION.md and
docs/public/ARCHITECTURE.md.
Install
pip install choicegate
No runtime dependencies beyond the Python standard library. Three console entry points ship with
the package: choicegate-dispatch (family dispatch), choicegate-route (registry-bound
routing), and choicegate-rank (backward-compatible discovery ranking). Each reads strict JSON
from a file argument or - for stdin and writes one deterministic receipt. Builds are
deterministic and offline via the in-tree backend tools/choicegate_backend.py; see
docs/public/VALIDATION.md for the package and surface-package gates.
Status
0.2.0, alpha, prepared locally; PyPI still serves 0.1.0. The portable 23-test suite and CI pass
offline, including the synthetic full-router profile; the owner-registry replay remains
maintainer-only. Selection is never execution; the full list of actions this
project will never take is in STATUS.md.
Governance
ChoiceGate is available under the MIT License. Contributions follow CONTRIBUTING.md, the Code of Conduct, and private security reporting guidance. Planned work and next-release gates are in ROADMAP.md.
Built by Rahul Krishna.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file choicegate-0.2.0.tar.gz.
File metadata
- Download URL: choicegate-0.2.0.tar.gz
- Upload date:
- Size: 121.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d09c5297e220f7843ac29c885f3f7cea8120aa1d78a2fba8d858768157445028
|
|
| MD5 |
8ccc5a2c30c6bcf855d24041c6ff75c0
|
|
| BLAKE2b-256 |
7e391c61ed5bc35ba41bb722f1c02f6d65a7bebff77a7cf0ef09c553a7c02b10
|
File details
Details for the file choicegate-0.2.0-py3-none-any.whl.
File metadata
- Download URL: choicegate-0.2.0-py3-none-any.whl
- Upload date:
- Size: 43.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3217bfbcd5f332129bb23a0152c6718451b539313ba97bdc742c02c0b0fac703
|
|
| MD5 |
1f015b7169dd3cbfc3a52cf51ad00514
|
|
| BLAKE2b-256 |
6a3ae55251e452791700c4efdf0b850976775e1fa6fa118f6483e2581cdcca37
|