Skip to main content

Deterministic capability-selection gate: select one safe, evidence-bound capability path or fail closed.

Project description

ChoiceGate logo

ChoiceGate

pipeline status PyPI version License: MIT

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/ChoiceGate and 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, and choicegate-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.

ChoiceGate returns one receipt and stops before execution

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

choicegate-0.2.0.tar.gz (121.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

choicegate-0.2.0-py3-none-any.whl (43.1 kB view details)

Uploaded Python 3

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

Hashes for choicegate-0.2.0.tar.gz
Algorithm Hash digest
SHA256 d09c5297e220f7843ac29c885f3f7cea8120aa1d78a2fba8d858768157445028
MD5 8ccc5a2c30c6bcf855d24041c6ff75c0
BLAKE2b-256 7e391c61ed5bc35ba41bb722f1c02f6d65a7bebff77a7cf0ef09c553a7c02b10

See more details on using hashes here.

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

Hashes for choicegate-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3217bfbcd5f332129bb23a0152c6718451b539313ba97bdc742c02c0b0fac703
MD5 1f015b7169dd3cbfc3a52cf51ad00514
BLAKE2b-256 6a3ae55251e452791700c4efdf0b850976775e1fa6fa118f6483e2581cdcca37

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page