omp-verdict-tiered0.15.0-omp.1-hotfix
Tiered permission gate for omp (oh-my-pi): local LANCET-Nano triage plus a model adjudicator, inspired by Claude Code auto mode
Built for omp
The author tagged this package oh-my-pi.
Reports 0
No reports yet.
Readme
omp-verdict-tiered
A minimal permission gate for omp (oh-my-pi), inspired by Claude Code's auto mode: every tool call gets checked before it runs — allow, deny, or ask you first.
Fork notice. This is
omp-verdict-tiered, a fork of jesset/pi-verdict by Jesset, ported to run on omp only. The original project targets pi; all credit for the design, the rule floor, the self-protection layer, and the research behind them belongs to the original author. See Attribution.
- Built-in danger rules and your own allow/deny rules settle the clear cases first, at zero latency
- Everything else goes to a model classifier that sees the conversation context
- Optional tiered adjudication: a local ONNX model triages shell commands before any network call
- Any uncertainty or failure fails closed; nothing ever runs silently
- Self-protection: the gate guards itself against snooping and tampering
The problem
omp ships with no per-call permission prompt — every tool call executes with the permissions of the omp process. This extension adds the missing gate: a model decides whether each call should run, based on the conversation context and your intent.
Why three states
A verdict is an adjudication, not a switch. Most classifiers in this space output a binary allow/block. Three states matter: ask routes genuinely ambiguous actions to a human (and degrades to deny in non-interactive sessions), so "not sure" never silently becomes "go ahead" — the goal is safe automation, not maximum automation: both approval fatigue and silent unsafe execution lose.
Design principles
- Fail closed — uncertainty produces friction, never permission.
- Deterministic floor before AI — hard denies are never overridden by the classifier or user allow rules.
- Semantics over syntax — the classifier judges what an action does, not how long it is.
- Judgments, not proofs — a classifier
allowis an informed opinion; the floor exists because that is all it is. - Minimal trusted input — no tool results in the transcript, zero path plaintext to the classifier (ADR-0002).
- Canonical identity — lexical + realpath dual-form matching; a workspace-looking path is not trusted as one.
- The gate guards itself — self-protection that no configuration can disable (ADR-0001).
- A permission gate, not a sandbox — stack OS isolation on top; this gate never replaces it.
Full statement in docs/security-principles.md.
Quick start
# 1. install the plugin (pulls the ONNX runtime it needs)
omp plugin install omp-verdict-tiered
# 2. download the Tier 1 model bundle (~111 MB, hash-verified)
bun run --cwd ~/.omp/plugins/node_modules/omp-verdict-tiered setup:lancet
Then restart omp. The gate is on by default (--no-auto-mode disables it); /automode shows status.
bun run --cwd ... setup:lancet is required for tiered adjudication (semanticAdjudication): without the bundle, Tier 1 cannot run and shell commands escalate to the Tier 2 model on every gray-zone call. Skip it if you only use the rule layer plus the first-layer classifier.
From a checkout instead (for development, or to pin a commit):
git clone git@github.com:alfiwillianz/omp-verdict-tiered.git
cd omp-verdict-tiered && bun install
bun run setup:lancet
omp plugin install "$PWD" # loaded in place; no copy
omp --extension "$PWD" # or: one invocation, no plugin entry
Full guide — install forms, the Tier 1 bundle, updating, troubleshooting: docs/installation.md.
Requires omp ≥ 18.1 and Bun. Works in interactive and non-interactive (-p/json/rpc) sessions; in non-interactive modes ask degrades to deny. The install is ~530 MB on disk, almost all of it onnxruntime-node's prebuilt binaries — see the install guide before installing on a small machine.
Host layout
| omp | |
|---|---|
| install | omp plugin install omp-verdict-tiered, or "$PWD" from a clone (guide) |
| plugin copy | ~/.omp/plugins/node_modules/omp-verdict-tiered/ (omp ≥ 18.1; ≤18.0: under agent/) |
| user rules | ~/.omp/agent/config/omp-verdict.json |
| credential store (S0 hard deny) | ~/.omp/agent/agent.db, ~/.omp/agent/secrets.yml |
| extension discovery | ~/.omp/agent/extensions/, <project>/.omp/extensions/ |
/automode— show current status: on/off/automode on/automode offctrl+shift+a— toggle the master switch silently (the always-on footer is the only feedback; rebind or disable viatoggleShortcut)- footer always shows
auto mode on(green) /auto mode off(yellow)
Config commands
Each /verdict-* command edits the global config (~/.omp/agent/config/omp-verdict.json) and applies to new sessions (or /reload). Bare invocation prints the current value and usage.
| Command | Effect |
|---|---|
/verdict-status |
effective configuration, resolved paths, and the active tiers |
/verdict-timeout <100-60000> |
Tier 2 adjudicator ceiling in ms (default 20000) |
/verdict-role <role> |
Tier 2 model role; completion lists omp's chat roles plus any modelRoles you configured |
/verdict-model <spec|@role|none> |
first-layer classifier model; none = self-reflection |
/verdict-floor <0-100|none> |
confidence floor for judgment-kind classifiers |
/verdict-tier1 on|off |
local LANCET-Nano triage |
/verdict-role completes against omp's built-in chat roles (adjudicator, smol, slow, default, task, advisor) and any custom role found in config.yml's modelRoles. Judge/image/web kind roles are excluded — Tier 2 needs a model that answers JSON.
| Option | Default | Description |
|---|---|---|
--auto-mode / --no-auto-mode |
on | master switch |
--auto-mode-model provider/id[:thinking] |
session model | classifier model, or @role (e.g. @smol) |
--auto-mode-debug |
off | full verdict notifications |
OMP_AUTO_MODE_MODEL |
— | env form of the model flag |
OMP_AUTO_MODE_DEBUG=1 |
off | env form of debug (flag wins) |
User rules (~/.omp/agent/config/omp-verdict.json)
{
"allow": ["^ls\\b", "^git (status|log|diff)\\b"],
"deny": ["rm ", "docker ", "^/etc/"],
"denyPaths": [
"~/.ssh/",
"~/.profile",
"~/.gnupg",
"~/.mc",
"~/.kube",
"~/.zshrc",
"~/.bashrc"
],
"ignoreTools": [
"todo",
"ask",
"memory_write",
"memory_search"
],
"builtinDenyFloor": true,
"classifierModel": null,
"toggleShortcut": "ctrl+shift+a",
"audit": false,
"auditRedactSecrets": true,
"notifyAllows": false,
"classifierMinConfidence": null,
"classifierFallbackModel": null,
"classifierFallbackMode": "enforce",
"semanticAdjudication": {
"tier1Lancet": {
"enabled": true,
"modelPath": "~/.cache/omp/lancet-nano",
"riskyThreshold": 0.9912,
"riskyAction": "ask"
},
"tier2Role": "adjudicator",
"tier2TimeoutMs": 20000
}
}
allow/denyare JS regex arrays;denywins overallow, both beat the classifierdenyPathsare plain paths you declare protected — touches trigger a terminal ask you adjudicate (non-interactive → deny); the classifier never learns the paths themselves, only that they exist.grep/glob/find/ast_grepcompare their whole search scope: an omittedpath(omp's default: the workspace root) or a parent directory of a declared path triggers the ask as well. A fresh install pre-fills a starter list (~/.ssh/,~/.gnupg/,~/.mc, shell rc/profile files)ignoreToolsnames uncovered tools (todo,ask, MCP/custom tools) that skip adjudication — allow with zero model calls; entries naming covered tools (bash/read/write/edit/grep/glob/find/ast_edit/ast_grep) are inert: those stay governed by the deny floor and your allow/deny rules, and the self-protection layer always runs firstbuiltinDenyFloor: falseturns off the built-in danger/path floor (your risk; the self-protection layer below always stays on)classifierModelpins the classifier model, e.g."zai/glm-5.3-flash:low"(thinking suffix supported; default: session model with thinking off). It also accepts an omp role alias ("@smol","@slow", any configured role)semanticAdjudicationopts into tiered adjudication (ADR-0009): shell commands are triaged locally first by Tier 1 — LANCET-Nano, a 111 MB INT8 ONNX model on CPU, no network (setup).not_flaggedallows immediately (~10 ms),riskyasks (or denies withriskyAction: "block"),review/oversized input escalates to Tier 2, a model resolved from omp's model facade (tier2Role:"adjudicator"→"smol"→ session model) that sees the tool call, the conversation and Tier 1's band. Tier 2allowpasses;ask/denyboth go to you; any failure (no model, network, 20 s timeout, bad JSON) fails closed to ask (headless → deny). Omit the block and behavior is exactly 0.15's single-classifier pathaudit: truerecords every gray-zone adjudication (the full transcript sent to the classifier, its raw response, the parsed verdict) as JSONL under~/.omp/agent/verdicts/<sessionId>.jsonl— one file per session, the 20 most recent kept. Interactive asks also record your answer (userAnswerground truth, written after the confirm resolves), and protected-path asks are recorded too; rule allow/deny stays unaudited. Local-only and full-fidelity (protected-path plaintext may appear — it never leaves your machine); secret-shaped text is redacted on append (<redacted:type#fingerprint>, deterministic so cross-record tracing survives —auditRedactSecrets: falseopts out; ADR-0007)notifyAllows: truenotifies on every classifier allow (reason + action line); defaultfalsekeeps passes silent. Mechanical passes (your own allow rules, protected-path confirms) never notifyclassifierMinConfidence(optional, ADR-0004) sets the confidence floor: a native-judgment verdict below it is demoted — cascaded toclassifierFallbackModelif set (enforce, the default = the second layer adjudicates; a demoted deny or ask can never be auto-relaxed to an allow;shadow= records its opinion only and you are asked), otherwise asked of you directly. At/above the floor the first layer is autonomous. The floor applies to judgment-kind models only (protocol-native confidence) — with a chat model it is inertclassifierFallbackModel/classifierFallbackModecontrol the second layer (ADR-0004)
No built-in allowlist — every "always allow" claim is yours (why). Full reference: docs/configuration.md.
Tier 1 Lancet-Nano setup
bun run setup:lancet # ~/.cache/omp/lancet-nano (pinned v0.4.3, hash-verified)
bun run setup:lancet --check # verify an existing bundle, no network
bun run setup:lancet --dir DIR # custom location (must match tier1Lancet.modelPath)
The setup verifies export.json against the release manifest digest, every model file against export.json, and the tokenizer against the reference implementation. The runtime repeats the file check on every load — a tampered or missing bundle never produces a not_flagged; Tier 1 escalates with a one-shot warning. With the block configured but the bundle absent, shell commands go to Tier 2.
Classifier models on omp
The classifier runs on omp's chat completion path (completeSimple) with the session credential resolver, so OAuth refresh and key rotation stay host-owned. A judgment-kind model (typesafe/jev-latest and the other *-decisions / systemone APIs) answers through omp's typed-judgment client instead: one structured call returning a choice, probabilities, and a confidence — which is what the confidence floor gates on.
{ "classifierModel": "typesafe/jev-latest" }
- TypeSafe direct:
export TYPESAFE_API_KEY=apikey_...(self-service at console.typesafe.ai) - OpenRouter:
/login openrouterinside omp, orexport OPENROUTER_API_KEY=sk-or-v1... - also served on Cloudflare Workers AI and OpenAI Decisions with each provider's login
Notes:
- Verdicts from judgment models keep the historical
jev:probability line; chat models renderclassifier: - A thinking suffix on a judgment model warns once and is dropped (no reasoning to configure)
- Chat models are addressed as
provider/id, a bare id, or a role alias (@smol,@slow, any configured role)
Self-protection (the gate guards itself — ADR-0001)
The gate's own files — the config and the installed extension copy — are user-editable only: writes from inside the gate hard-deny (reads pass); your editor never passes through the gate, the sudoers/visudo precedent.
- Not disableable by any config —
builtinDenyFloor: falseand userallowrules cannot touch this layer - Tamper detection as the backstop: watched files are snapshotted at
session_startand re-verified before every verdict — a changed extension copy is auto-restored and the session goes fail-closed; a changed config gets one explicit keep/restore confirm when a UI is available (headless auto-restores; ADR-0001 for the differential-disposal rationale)
Pipeline
tool_call
│
├─ 0. Self-protection layer (ADR-0001; not disableable by any config)
│ ├─ write/edit/ast_edit/bash touching the gate's own files → deny; reads pass
│ └─ tamper detection: re-verify before every verdict →
│ auto-restore + fail-closed, or one keep/restore confirm
│
├─ 1. Rule layer (deterministic, zero latency)
│ ├─ built-in deny floor: bash danger regexes + path sensitivity S0–S5
│ ├─ your rules: user deny beats user allow
│ ├─ denyPaths (ADR-0002): protected paths → terminal ask,
│ │ before user allow; classifier sees an existence hint only
│ ├─ ignoreTools: your declared uncovered tools → allow, zero model calls
│ └─ no built-in allowlist on your surfaces — every "always allow"
│ claim is yours to make
│
├─ 2. Gray zone → semantic adjudication
│ ├─ default: one classifier — a judgment model (choice + probabilities +
│ │ confidence; tag-free reason, full contract line in the audit
│ │ rawResponse) or, on the chat path (session-model self-reflection),
│ │ <verdict>…</verdict> prefix-anchored free text
│ └─ tiered (ADR-0009, opt-in semanticAdjudication block):
│ shell → Tier 1 LANCET-Nano (local, ~10 ms):
│ not_flagged → allow; risky → ask (or deny); review/long → Tier 2
│ all tools → Tier 2 adjudicator (role → smol → session):
│ strict JSON; allow passes, ask/deny → human; failure → ask
│
└─ 3. Three-state adjudication
├─ allow → pass
├─ deny → block, reason returned to the agent
└─ ask → human confirm; non-interactive modes degrade to deny
fail-closed: classifier exception / timeout (25s) / contract violation → deny; in tiered mode, Tier 2 failure (no model, network, 20s timeout, bad JSON) → ask (headless → deny). Never silently allow.
Status & limitations
- no built-in allowlist on user surfaces by design (see the bypass writeup); with an empty
allowconfig most commands go to the classifier — point--auto-mode-modelat a fast model if per-call latency matters - the path sensitivity floor applies to file tools only: bash command strings are matched by the danger regexes alone, so e.g.
cat ~/.ssh/id_rsagoes to the classifier rather than the deterministic S0 deny (the file-tool spellingread ~/.ssh/id_rsadoes deny) - on Windows the built-in floor covers bash-shaped patterns only — PowerShell-native dangerous commands (
Remove-Item -Recurse -Force,Invoke-Expression,Set-ExecutionPolicy, …) rely on the classifier (fail-closed) - on macOS the per-user temp tree (
$TMPDIR, the/var/folders/…/Tconfstr dir) is exempt from the system-directory floor: reads allow at zero cost, writes adjudicate as ordinary outside-project writes (classifier). The exemption anchors to the runtime-resolved confstr family — a hand-setTMPDIRlifts nothing — and/var/tmp(POSIX shared temp) stays denied; symlink spellings whose real form escapes the temp tree still hit S1 AGENTS.mdis not passed to the classifier as downweighted intent evidence (Claude Code does this)- parallel gray-zone calls are adjudicated serially
- self-reflection means the session model adjudicates — point
--auto-mode-modelat a lighter model if verdict latency/cost matters denyPathsbash extraction is token-level (ADR-0002): command substitution, base64-embedded paths and external script contents produce no hit signal — those calls fall back to the classifier's existence-hint vigilance. MCP and custom tools bypass the extractor entirely (their gray-zone adjudication still carries the hint). Path normalization is base-tier only: a nonexistent target written through a symlinked directory rebuilds no real form and produces no hitdenyPathsbash tokens contain no spaces: a declared path containing spaces cannot be spelled in a bash command in a way the extractor sees. A recursive search issued from a shell misses in both spellings (no path argument, or a parent-directory argument); the file tools' bidirectional comparison covers those cases- self-protection bash matching is substring regex — obfuscatable; the tamper-detection backstop catches within-session bypasses, but a cross-session baseline is phase 2 per ADR-0001
- dev checkouts (running the extension from a repo) are not self-protected — the installed copy the next normal session loads is only covered by its own sessions' gate
- the classifier is one model call per gray-zone decision;
classifierMinConfidence+classifierFallbackModeladd a second opinion at the cost of a second call - omp-only. This fork dropped the pi host: pi's
classify()protocol,getModelForRole(), the codemode nested-call policy, and the headless subagent-artifacts exemption do not exist on omp and are not implemented here
The gate is not a sandbox. It runs inside the omp process and adjudicates tool calls; it does not contain malicious code, protect against a compromised process, or guard manual ! shell escapes. For isolation, use an OS-level sandbox.
The name: the three-state verdict is the core concept. The UX keeps /automode — the mode concept traces back to Claude Code's auto mode, which this project borrows its transcript design from.
Development
bun install
bun run typecheck
bun test # offline stub suite: self-protection, tamper detection, deny floor, user rules, denyPaths, bypass regression, classifier retry, tiered pipeline, commands, toggle shortcut
Issue tracker and decision records live in the GitHub issues.
Attribution
This fork exists because of Jesset's original work on pi-verdict (MIT). The rule floor, the denyPaths design, the self-protection layer, the classifier transcript contract, and the research notes under research/ are theirs. This fork's changes are the omp port (host API adaptation, the judgment/chat classifier seams, omp tool names and credential paths) and the tiered adjudication work.
If you are on pi rather than omp, use the upstream project.
License
MIT — original copyright © 2026 Jesset; fork modifications under the same license.