pi-opa-net 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,37 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-07-01
11
+
12
+ ### Added
13
+
14
+ - **decision-output.v1 schema** — JSON Schema draft 2020-12, strict (`additionalProperties: false`). Symmetric allow + deny output with rule provenance, fail-mode observability, and parse-confidence surfacing. 4 canonical examples, all validated by a hard test gate.
15
+ - **OPA decision engine** (`OpaCliEngine`) — subprocess `opa eval` with temp-file input, fail-open/fail-closed branching, SHA-256 rulebook digest for drift detection.
16
+ - **Hybrid command parser** — `ShellQuoteParser` (AST primary) + `RegexFallbackParser` (fallback), coordinated via `CommandParserCoordinator`. Program-aware subcommand classification (git/docker/gh/glab subcommand-style; rm/bd/gcloud/bq args-only).
17
+ - **Rule registry + 37-rule catalog** mirroring `policy/safety.rego` message-for-message. A bidirectional parity test enforces zero drift between rego and the TS catalog.
18
+ - **CLI** (`pi-opa-net eval`) — claude-code mode (suppress allow stdout) and `--json` mode (always emit schema). Exit codes `0 = allow`, `2 = deny` (Claude Code hook protocol compatible). Reads from args or stdin.
19
+ - **Rego policy** (`policy/safety.rego`) — covers git, docker, docker-compose carve-outs, rm, gh, glab, gcloud, bq, bd families. Native bare-default handling (`git stash` ≡ push).
20
+ - **Env-driven config** — `PI_OPA_FAIL_MODE`, `PI_OPA_TIMEOUT_MS`, `PI_OPA_BINARY` (mise-aware discovery), `PI_OPA_HOSTNAME`, `PI_OPA_SESSION_ID`.
21
+ - **Decision-design docs** — `docs/locked-decisions.yaml` (LD1–LD5), `docs/open-threads.yaml` (OT1–OT5, all resolved with rationale).
22
+ - **CI** — GitHub Actions workflow (typecheck + lint + test + coverage on ubuntu/macos).
23
+ - **Skill doc** — `skills/pi-opa-net/SKILL.md` for pi agent discovery.
24
+
25
+ ### Resolved design threads
26
+
27
+ - **OT1 (parser)** — hybrid: AST primary, regex fallback; `parse_confidence` surfaces path per-decision.
28
+ - **OT2 (fail-mode)** — fail-open default (matches pi-safety-net fork), configurable to fail-closed.
29
+ - **OT3 (bare git stash)** — handled natively in rego (`subcommand == "stash" && count(args) == 0`).
30
+ - **OT4 (fork disposition)** — pi-safety-net kept as Path A (non-pi agents); pi-opa-net is Path B (OPA-backed).
31
+ - **OT5 (pi extension wiring)** — deferred to a separate `pi-opa-net-ext` repo; this package exposes the engine + library + CLI.
32
+
33
+ ### Tests
34
+
35
+ - 106 tests across 10 files (unit + e2e + schema gate).
36
+ - Line coverage 98.89%, function coverage 88.98%.
37
+ - E2E runs the live CLI against real OPA 1.18.1 — 20 distinct deny rules fire (≥40% of the 37-rule catalog) plus 5 allow carve-outs and fail-open/fail-closed paths.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 buihongduc132
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,203 @@
1
+ # pi-opa-net
2
+
3
+ [![npm version](https://img.shields.io/npm/v/pi-opa-net)](https://www.npmjs.com/package/pi-opa-net)
4
+ [![CI](https://github.com/buihongduc132/pi-opa-net/actions/workflows/ci.yml/badge.svg)](https://github.com/buihongduc132/pi-opa-net/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
6
+
7
+ > OPA-backed bash command guard for the [Pi](https://pi.dev) ecosystem. Structured `--json` output (decision-output.v1 schema), fail-open default, exit-code compatible with the Claude Code hook protocol.
8
+
9
+ An **agent-agnostic** engine + CLI that evaluates shell commands against an [OPA](https://www.openpolicyagent.org/)/Rego policy and emits a strict, auditable decision record. Designed as the decision backend for pi extensions, Claude Code hooks, scripts, and any agent that needs a uniform bash-guard contract.
10
+
11
+ ## Why
12
+
13
+ Three limitations of today's asymmetric, agent-specific guard output that this fixes:
14
+
15
+ | # | Limitation | Fix |
16
+ |---|------------|-----|
17
+ | 1 | **Asymmetric** — allow is silent, deny emits a string | Both allow AND deny emit the full schema |
18
+ | 2 | **No provenance** — only a human message | `reasons[].rule_id` traces decision → rule → source line |
19
+ | 3 | **Agent-specific** — tied to one hook protocol | Agent-agnostic wrapper; adapters become thin views |
20
+
21
+ ## Status
22
+
23
+ - **Stable:** v0.1.0 — schema v1.0, 37-rule catalog, full TDD coverage
24
+ - **Engine:** OPA 1.x (lazy-loaded on every dev box)
25
+ - **Scope:** bash command guarding only (see [`docs/locked-decisions.yaml`](docs/locked-decisions.yaml) LD3)
26
+ - **Pi extension:** the thin tool_call adapter lives in a separate future repo (`pi-opa-net-ext`, per OT5) — this package is the engine + library
27
+
28
+ ## Installation
29
+
30
+ ### Prerequisites
31
+
32
+ OPA 1.x on `PATH` (recommended via [mise](https://mise.jdx.dev)):
33
+
34
+ ```bash
35
+ mise install opa@latest
36
+ mise use -g opa@latest
37
+ ```
38
+
39
+ ### Install
40
+
41
+ ```bash
42
+ # as a library (pi extension / script consumer)
43
+ npm install pi-opa-net
44
+ # or
45
+ bun add pi-opa-net
46
+
47
+ # run the CLI directly via bun
48
+ bunx pi-opa-net eval "git stash pop"
49
+ ```
50
+
51
+ ## Usage
52
+
53
+ ### CLI
54
+
55
+ ```bash
56
+ # claude-code mode (default): suppress stdout on allow, JSON on deny
57
+ pi-opa-net eval "git stash pop" # exit 2 + JSON on stdout
58
+ pi-opa-net eval "git stash list" # exit 0, empty stdout
59
+
60
+ # --json: always emit the full decision-output.v1 schema
61
+ pi-opa-net eval "git stash pop" --json
62
+
63
+ # stdin
64
+ echo "docker stop foo" | pi-opa-net eval
65
+ ```
66
+
67
+ **Exit codes:** `0 = allow`, `2 = deny` (Claude Code hook protocol compatible).
68
+
69
+ ### Programmatic API
70
+
71
+ ```ts
72
+ import { configFromEnv, CommandParserCoordinator, OpaCliEngine, DecisionBuilder, OutputFormatter, RULES, RuleRegistry } from 'pi-opa-net';
73
+
74
+ const config = configFromEnv('/path/to/safety.rego');
75
+ const parser = new CommandParserCoordinator();
76
+ const engine = new OpaCliEngine(config);
77
+ const builder = new DecisionBuilder({
78
+ config,
79
+ registry: new RuleRegistry(RULES),
80
+ digest: engine.rulebookDigest(),
81
+ });
82
+
83
+ const parsed = parser.parse('git stash pop');
84
+ const engineDecision = await engine.evaluate(parsed);
85
+ const output = builder.build(parsed, engineDecision);
86
+
87
+ console.log(output.decision); // 'deny'
88
+ console.log(output.reasons[0].rule_id); // 'block-git-stash-mutations'
89
+ ```
90
+
91
+ ## Output schema
92
+
93
+ See [`schemas/decision-output.v1.json`](schemas/decision-output.v1.json) — JSON Schema draft 2020-12, strict (`additionalProperties: false` throughout). Every emitted record is validated against it before leaving the process.
94
+
95
+ ```jsonc
96
+ {
97
+ "schema_version": "1.0",
98
+ "decision": "deny", // allow | deny
99
+ "action": "block", // allow | block | prompt_user(v2) | log_only(v2)
100
+ "source": "opa", // opa | fail-open | fail-closed | cached
101
+ "reasons": [ // every fired deny rule → one entry
102
+ { "rule_id": "block-git-stash-mutations",
103
+ "message": "Do not mutate stashes in shared work...",
104
+ "family": "git", "severity": "block" }
105
+ ],
106
+ "input": { "raw": "git stash pop", "program": "git",
107
+ "subcommand": "stash", "args": ["pop"],
108
+ "parse_confidence": "full" }, // full | partial | regex-only | failed
109
+ "summary": "BLOCKED: git stash pop (rule: block-git-stash-mutations)",
110
+ "suggestions": ["git stash list", "git stash show"],
111
+ "metadata": { "engine": "opa", "opa_version": "1.18.1",
112
+ "rulebook_digest": "dee3746bf7b5", "policy_path": "...",
113
+ "hostname": "box", "session_id": "" },
114
+ "evaluated_at": "2026-07-01T14:23:45.123Z",
115
+ "decision_id": "7f3a9c2e-1b4d-4e8f-9a2c-5d6e7f8a9b01",
116
+ "duration_ms": 4.2
117
+ }
118
+ ```
119
+
120
+ ## Architecture
121
+
122
+ Two halves (per the design findings):
123
+
124
+ | Half | Responsibility | Module |
125
+ |------|----------------|--------|
126
+ | **Parse** | raw `"git stash list"` → `{program, subcommand, args, parse_confidence}` | `src/parser/` |
127
+ | **Decide** | structured input → allow/deny + reasons | `policy/safety.rego` + `src/engine/` |
128
+
129
+ ```
130
+ src/
131
+ ├── parser/ CommandParserCoordinator (hybrid: ShellQuote AST primary, regex fallback)
132
+ ├── engine/ OpaCliEngine (subprocess `opa eval` + fail-mode)
133
+ ├── rules/ RuleRegistry + catalog (message → rule_id + family provenance)
134
+ ├── output/ DecisionBuilder (schema assembly) + OutputFormatter (stdout/exit-code)
135
+ ├── config/ EngineConfig (fail-mode, OPA binary discovery)
136
+ ├── cli/ run.ts (wires the pipeline)
137
+ └── util/ sha256Prefix (rulebook drift detection)
138
+ ```
139
+
140
+ ### Design principles
141
+
142
+ - **OOP** — `DecisionEngine` and `CommandParser` interfaces; fakes injectable for tests.
143
+ - **DRY** — `RuleRegistry` is the single source of truth for rule provenance; the catalog mirrors `policy/safety.rego` message-for-message (a parity test fails on drift).
144
+ - **Observable** — the `source` field makes fail-mode (open/closed) auditable per-decision; `parse_confidence` surfaces parser fidelity.
145
+
146
+ ## Fail-mode
147
+
148
+ When OPA is unreachable (cold-start, binary missing, timeout):
149
+
150
+ | Mode | Behavior | `source` field |
151
+ |------|----------|----------------|
152
+ | `open` (default) | allow the command through | `fail-open` |
153
+ | `closed` | block the command | `fail-closed` |
154
+
155
+ ```bash
156
+ PI_OPA_FAIL_MODE=closed pi-opa-net eval "git stash pop"
157
+ ```
158
+
159
+ The default `open` matches the [`pi-safety-net`](https://www.npmjs.com/package/pi-safety-net) fork's "never brick the shell" guarantee.
160
+
161
+ ## Configuration (env)
162
+
163
+ | Var | Default | Purpose |
164
+ |-----|---------|---------|
165
+ | `PI_OPA_BINARY` | auto (PATH → mise) | OPA binary path |
166
+ | `PI_OPA_FAIL_MODE` | `open` | fail-mode |
167
+ | `PI_OPA_TIMEOUT_MS` | `250` | OPA eval timeout |
168
+ | `PI_OPA_HOSTNAME` | `os.hostname()` | metadata.hostname |
169
+ | `PI_OPA_SESSION_ID` | `""` | metadata.session_id |
170
+
171
+ ## Develop
172
+
173
+ ```bash
174
+ bun install
175
+ bun test # all tests (106)
176
+ bun test --coverage # coverage (line > 98%)
177
+ bun run typecheck # tsc --noEmit
178
+ bun run lint # biome
179
+ bun run smoke # one-shot CLI check
180
+ ```
181
+
182
+ E2E tests run the live CLI against real OPA + the real policy, covering ≥40% of the 37-rule catalog.
183
+
184
+ ## Decisions & open threads
185
+
186
+ - [`docs/locked-decisions.yaml`](docs/locked-decisions.yaml) — LD1–LD5 (immutable inputs).
187
+ - [`docs/open-threads.yaml`](docs/open-threads.yaml) — OT1–OT5 resolved at implementation time.
188
+
189
+ ## Project health
190
+
191
+ - **Security policy:** [`SECURITY.md`](SECURITY.md)
192
+ - **Contributing:** [`CONTRIBUTING.md`](CONTRIBUTING.md)
193
+ - **Support:** [`SUPPORT.md`](SUPPORT.md)
194
+ - **Changelog:** [`CHANGELOG.md`](CHANGELOG.md)
195
+
196
+ ## Related
197
+
198
+ - [`pi-safety-net`](https://www.npmjs.com/package/pi-safety-net) — the fail-open fork of cc-safety-net (Path A: non-pi agents). pi-opa-net is Path B (OPA-backed, structured output).
199
+ - [`cc-safety-net`](https://www.npmjs.com/package/cc-safety-net) — upstream Claude Code safety net.
200
+
201
+ ## License
202
+
203
+ MIT © [buihongduc132](https://github.com/buihongduc132)
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * pi-opa-net — OPA-backed bash command guard with structured --json output.
4
+ *
5
+ * Usage:
6
+ * pi-opa-net eval "<command>" # claude-code mode (default)
7
+ * pi-opa-net eval "<command>" --json # full schema on stdout always
8
+ * echo "<command>" | pi-opa-net eval # read from stdin
9
+ *
10
+ * Exit codes: 0=allow, 2=deny (Claude Code hook protocol compatible).
11
+ */
12
+ import { defaultPolicyPath, runCli } from '../src/cli/run.ts';
13
+
14
+ function parseArgs(argv) {
15
+ const args = argv.slice(2); // drop node + script
16
+ let mode = 'claude-code';
17
+ let command;
18
+ let policyPath = defaultPolicyPath();
19
+ let sawAction = false;
20
+
21
+ for (let i = 0; i < args.length; i++) {
22
+ const a = args[i];
23
+ if (a === '--json') {
24
+ mode = 'json';
25
+ } else if (a === '--policy' || a === '-p') {
26
+ policyPath = args[++i];
27
+ } else if (a === '--help' || a === '-h') {
28
+ printHelp();
29
+ process.exit(0);
30
+ } else if (!a.startsWith('-')) {
31
+ // First positional is the action verb ('eval'); subsequent ones form the command.
32
+ if (!sawAction && a === 'eval') {
33
+ sawAction = true;
34
+ } else {
35
+ command = command === undefined ? a : `${command} ${a}`;
36
+ }
37
+ }
38
+ }
39
+ return { command, mode, policyPath };
40
+ }
41
+
42
+ function printHelp() {
43
+ console.error(`pi-opa-net — OPA-backed bash command guard
44
+
45
+ Usage:
46
+ pi-opa-net eval "<command>" [--json] [--policy <path>]
47
+ echo "<command>" | pi-opa-net eval
48
+
49
+ Modes:
50
+ (default) claude-code: suppress stdout on allow, JSON on deny
51
+ --json always emit full decision-output.v1 schema on stdout
52
+
53
+ Exit codes: 0=allow, 2=deny`);
54
+ }
55
+
56
+ async function main() {
57
+ const { command, mode, policyPath } = parseArgs(process.argv);
58
+ const { stdout, exitCode } = await runCli({ command, mode, policyPath });
59
+ if (stdout) {
60
+ console.log(stdout);
61
+ }
62
+ process.exit(exitCode);
63
+ }
64
+
65
+ void main();
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "pi-opa-net",
3
+ "version": "0.1.0",
4
+ "description": "OPA-backed bash command guard for the pi ecosystem — structured decision-output.v1 JSON, fail-open default, Claude Code hook protocol compatible. Agent-agnostic engine + CLI.",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "bin": {
9
+ "pi-opa-net": "bin/pi-opa-net.js"
10
+ },
11
+ "files": [
12
+ "src",
13
+ "bin",
14
+ "schemas",
15
+ "policy",
16
+ "skills",
17
+ "README.md",
18
+ "CHANGELOG.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "typecheck": "tsc --noEmit",
23
+ "lint": "biome check --write src tests bin",
24
+ "lint:ci": "biome ci src tests bin",
25
+ "test": "bun test",
26
+ "test:coverage": "bun test --coverage",
27
+ "check": "bun run typecheck && bun run lint:ci && bun test --coverage",
28
+ "smoke": "bun run bin/pi-opa-net.js eval \"git stash pop\" --json",
29
+ "prepublishOnly": "bun run check"
30
+ },
31
+ "keywords": [
32
+ "pi-package",
33
+ "pi",
34
+ "pi-coding-agent",
35
+ "pi-library",
36
+ "opa",
37
+ "rego",
38
+ "bash-guard",
39
+ "safety-net",
40
+ "claude-code",
41
+ "policy",
42
+ "decision-output"
43
+ ],
44
+ "pi": {
45
+ "skills": ["./skills"]
46
+ },
47
+ "dependencies": {
48
+ "ajv": "^8.17.1",
49
+ "ajv-formats": "^3.0.1",
50
+ "shell-quote": "^1.8.2"
51
+ },
52
+ "devDependencies": {
53
+ "@biomejs/biome": "^1.9.4",
54
+ "@types/bun": "latest",
55
+ "@types/shell-quote": "^1.7.5",
56
+ "typescript": "^5.6.0"
57
+ },
58
+ "engines": {
59
+ "bun": ">=1.1.0"
60
+ },
61
+ "license": "MIT",
62
+ "author": "buihongduc132",
63
+ "homepage": "https://github.com/buihongduc132/pi-opa-net#readme",
64
+ "repository": {
65
+ "type": "git",
66
+ "url": "git+https://github.com/buihongduc132/pi-opa-net.git"
67
+ },
68
+ "bugs": {
69
+ "url": "https://github.com/buihongduc132/pi-opa-net/issues"
70
+ }
71
+ }