plan-auditor 2.4.4__tar.gz → 2.4.6__tar.gz
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.
Potentially problematic release.
This version of plan-auditor might be problematic. Click here for more details.
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/CHANGELOG.md +25 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/CITATION.cff +2 -2
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/PKG-INFO +1 -1
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/SECURITY.md +10 -3
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/SKILL.md +1 -1
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/docs/deployment-isolation.md +49 -5
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/docs/threat-model.md +12 -1
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/package.json +1 -1
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/pyproject.toml +1 -1
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/scripts/audit_check.py +6 -8
- plan_auditor-2.4.6/scripts/check_isolation.py +116 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/__init__.py +1 -1
- plan_auditor-2.4.6/tests/test_linux_check_os_isolation.py +150 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/.gitignore +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/CODE_OF_CONDUCT.md +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/CONTRIBUTING.md +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/LICENSE +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/README.md +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/bin/launcher_contract.test.js +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/bin/plan-auditor.js +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/bin/prepublish_gate.js +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/bin/prepublish_gate_contract.test.js +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/bin/prepublish_lifecycle.test.js +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/bin/publish_mode_contract.test.js +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/docs/architecture.md +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/docs/benchmark.md +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/docs/check_attestation_claims.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/docs/check_readme_size.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/docs/cli.md +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/docs/dependency-graph.md +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/docs/formal-planning.md +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/docs/index.md +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/docs/integrations.md +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/docs/quickstart.md +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/docs/readme_size_baseline.json +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/docs/release-status.md +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/docs/workspace-artifacts.md +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/examples/fib/.plan-auditor/plan.json +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/examples/fib/fib.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/examples/fib/request-source.json +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/examples/fib/test_fib.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/hooks/README.md +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/hooks/gate_hook.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/index.js +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/mkdocs.yml +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/references/plan-format.md +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/scripts/__init__.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/scripts/contract.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/scripts/exec_trust.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/scripts/integrity.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/scripts/plan_graph.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/scripts/stop_gate.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/adversarial.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/agents.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/agents_hardening.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/audit_session.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/cli.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/config.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/contracts.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/control_plane.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/coverage.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/daemon.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/events.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/evidence.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/formal_compiler.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/formal_planning.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/formal_semantics.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/gate.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/goals.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/integrity.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/lifecycle.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/lock_hardening.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/orchestrator.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/plan_verifier.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/plans.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/policies.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/priority.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/request_contract.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/requirements.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/seal_migration.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/sealing.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/watchdog.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/supervisor/workspace.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/request_fixture.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_adversarial.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_agent_registry_chain.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_agents_gate.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_audit_check.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_authenticated_integrity.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_cli.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_console_scripts.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_core_layers.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_daemon.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_dependency_graph.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_doc_transcripts.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_docs_metadata.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_events.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_exec_trust_gate.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_extra_layers.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_failure_injection.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_formal_compiler.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_formal_planning.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_formal_semantics.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_full_contract_hardening.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_hmac_check_environment.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_hooks.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_integration_hardening.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_npm_launcher.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_npm_registry_visibility.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_readme_quickstart.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_registry_lock_partial.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_request_contract_hardening.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_root_cause_fixes_v220.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/test_runtime_root_hardening.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/trusted_workspace.py +0 -0
- {plan_auditor-2.4.4 → plan_auditor-2.4.6}/tests/wheel_cli_smoke.py +0 -0
|
@@ -4,6 +4,31 @@ All notable changes to plan-auditor. Versions are published to PyPI and npm.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 2.4.6 — Release alignment
|
|
8
|
+
|
|
9
|
+
- Publishes the 2.4.5 tree to PyPI and npm. The 2.4.5 commit landed after
|
|
10
|
+
the 2.4.5 PyPI upload, so both registries were one commit behind main.
|
|
11
|
+
No behavioural change beyond 2.4.5 itself.
|
|
12
|
+
|
|
13
|
+
## 2.4.5 — Opt-in Linux privilege-separated check execution
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
- Add `PLAN_AUDITOR_CHECK_ISOLATION=required` with explicit non-root
|
|
18
|
+
`PLAN_AUDITOR_CHECK_UID` / `PLAN_AUDITOR_CHECK_GID`: root-run Linux
|
|
19
|
+
supervisors execute project-controlled checks under the named UID/GID with
|
|
20
|
+
supplementary groups dropped, while the trusted process retains HMAC signing.
|
|
21
|
+
- Require root-owned protected control state, trusted workspace ancestry,
|
|
22
|
+
root-owned private HMAC key material and non-writable key directories.
|
|
23
|
+
Missing/invalid isolation configuration fails closed.
|
|
24
|
+
- Restrict the child environment to a small, documented allowlist, excluding
|
|
25
|
+
HMAC credentials, unrelated API tokens, `HOME`, `PYTHONPATH` and SSH sockets.
|
|
26
|
+
- Require real root-run Linux CI regression tests proving a child cannot read
|
|
27
|
+
either a root-owned `0600` key file or `.plan-auditor` control state.
|
|
28
|
+
- Document required host configuration. This is a Linux UID permission boundary,
|
|
29
|
+
**not** a cross-platform container/VM, network or kernel sandbox; default
|
|
30
|
+
same-user mode continues to have its documented limits.
|
|
31
|
+
|
|
7
32
|
## 2.4.4 — HMAC check subprocess boundary
|
|
8
33
|
|
|
9
34
|
### Security
|
|
@@ -21,8 +21,8 @@ authors:
|
|
|
21
21
|
repository-code: "https://github.com/Furox-Art/plan-auditor"
|
|
22
22
|
url: "https://github.com/Furox-Art/plan-auditor"
|
|
23
23
|
license: MIT
|
|
24
|
-
version: 2.4.
|
|
25
|
-
date-released: "2026-10-
|
|
24
|
+
version: 2.4.6
|
|
25
|
+
date-released: "2026-10-11"
|
|
26
26
|
keywords:
|
|
27
27
|
- AI coding agents
|
|
28
28
|
- deterministic verification
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: plan-auditor
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.6
|
|
4
4
|
Summary: Deterministic verification and evidence auditing for AI coding agents: sealed plans, hash-chained evidence, and an independent completion gate
|
|
5
5
|
Project-URL: Homepage, https://github.com/Furox-Art/plan-auditor
|
|
6
6
|
Project-URL: Repository, https://github.com/Furox-Art/plan-auditor
|
|
@@ -147,9 +147,16 @@ As of the 2.4.4 source hardening, behavioral-check subprocesses no longer inheri
|
|
|
147
147
|
`PLAN_AUDITOR_HMAC_KEY` or `PLAN_AUDITOR_HMAC_KEY_FILE` from the verifier.
|
|
148
148
|
This reduces inadvertent environment-based credential exposure, but **does not
|
|
149
149
|
sandbox checks**. Project-controlled checks running as the supervisor's OS user
|
|
150
|
-
can still read any external key file that user can read.
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
can still read any external key file that user can read.
|
|
151
|
+
|
|
152
|
+
Version 2.4.5 adds an **opt-in Linux-only privilege boundary**. A trusted root-run
|
|
153
|
+
supervisor can require each behavioral check to execute as a separate, non-root
|
|
154
|
+
UID/GID with no supplementary groups; the child cannot read a root-only HMAC key
|
|
155
|
+
or protected control state. Unsafe or incomplete configuration refuses execution.
|
|
156
|
+
This is not automatic in normal mode and is not a container, seccomp or network
|
|
157
|
+
sandbox. Never grant the check user access to the supervisor's key or installed
|
|
158
|
+
trusted program. Setup instructions and limitations:
|
|
159
|
+
[deployment isolation](docs/deployment-isolation.md).
|
|
153
160
|
|
|
154
161
|
`plan-auditor integrity init` and `plan-auditor integrity status` manage a
|
|
155
162
|
keyed authentication of evidence, seals and the agent registry. Detection is only
|
|
@@ -3,7 +3,7 @@ name: plan-auditor
|
|
|
3
3
|
description: Invoke this skill by name as `plan-auditor` for non-trivial build/implement/fix work or whenever the user wants independent proof that an AI actually completed the requested work. It converts the task into explicit requirements and machine-verifiable steps, automatically compiles those structured requirements/dataflow into a sealed LLM-free STRIPS contract, proves every step with real command evidence, and blocks completion until every active plan has an intact contract and a fresh deterministic audit.
|
|
4
4
|
argument-hint: "<task description>"
|
|
5
5
|
metadata:
|
|
6
|
-
version: "2.4.
|
|
6
|
+
version: "2.4.6"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Plan Auditor — strict plan + independent verification
|
|
@@ -40,11 +40,55 @@ must not be able to open it. Start the supervisor service with:
|
|
|
40
40
|
PLAN_AUDITOR_HMAC_KEY_FILE=/var/lib/plan-auditor/keys/project.key
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
The key file
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
The key file must be outside the workspace and be private to the trusted
|
|
44
|
+
supervisor. Since 2.4.5, the stock CLI offers an **opt-in Linux privilege drop**
|
|
45
|
+
for each behavioral check (not a container sandbox). The supervisor must run as
|
|
46
|
+
root; the untrusted check executes as a dedicated, unprivileged OS user.
|
|
47
|
+
|
|
48
|
+
A minimal trusted-host layout is:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Run once as a trusted administrator. The workspace and all ancestors must
|
|
52
|
+
# be root-owned. A writable workspace directory must have the sticky bit.
|
|
53
|
+
sudo useradd --system --no-create-home --shell /usr/sbin/nologin pa-check
|
|
54
|
+
sudo install -d -o root -g root -m 1777 /srv/pa-workspace
|
|
55
|
+
sudo install -d -o root -g root -m 0700 /srv/pa-workspace/.plan-auditor
|
|
56
|
+
sudo install -d -o root -g root -m 0700 /var/lib/plan-auditor/keys
|
|
57
|
+
# Provision a strong, random project.key through a trusted mechanism,
|
|
58
|
+
# owned by root, mode 0600. Never commit or print the key.
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The parent directories of the key file must be root-owned and not writable
|
|
62
|
+
by group/other. Place the trusted Plan Auditor installation and Python entry
|
|
63
|
+
point **outside the agent-writable workspace**, also owned by root. Configure
|
|
64
|
+
a host-owned service or root-run invocation:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
sudo env \
|
|
68
|
+
PLAN_AUDITOR_HMAC_KEY_FILE=/var/lib/plan-auditor/keys/project.key \
|
|
69
|
+
PLAN_AUDITOR_CHECK_ISOLATION=required \
|
|
70
|
+
PLAN_AUDITOR_CHECK_UID="$(id -u pa-check)" \
|
|
71
|
+
PLAN_AUDITOR_CHECK_GID="$(id -g pa-check)" \
|
|
72
|
+
/usr/local/bin/plan-auditor audit /srv/pa-workspace
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The path `/usr/local/bin/plan-auditor` is illustrative; it must point to a
|
|
76
|
+
trusted root-managed installation. The host must separately activate and seal
|
|
77
|
+
its authoritative plan and retain ownership of `.plan-auditor`; the agent must
|
|
78
|
+
not control the supervisor service's environment. The check identity must not
|
|
79
|
+
be root and must not have access to the HMAC file or control-state directory.
|
|
80
|
+
|
|
81
|
+
This mode rejects unsupported platforms, non-root supervisors, invalid IDs,
|
|
82
|
+
world/group-readable key files, insecure key parents, and writable workspace
|
|
83
|
+
ancestors without root ownership and sticky-bit protection. A failure stops the
|
|
84
|
+
check with a nonzero result; it never silently falls back to same-user execution.
|
|
85
|
+
Ordinary installations without these variables retain the documented default,
|
|
86
|
+
which **does not protect against a deliberately malicious same-user process**.
|
|
87
|
+
|
|
88
|
+
This is a Linux user-permissions boundary, **not** a namespace, seccomp, resource
|
|
89
|
+
quota, or network sandbox. Host operators may additionally put the untrusted
|
|
90
|
+
check identity in a separately restricted container/VM and must not grant it
|
|
91
|
+
sensitive groups, writable privileged paths, ambient capabilities or sudo rights.
|
|
48
92
|
|
|
49
93
|
## Container/VM boundary
|
|
50
94
|
|
|
@@ -137,7 +137,18 @@ separate container/VM, give the untrusted agent write access only to the intende
|
|
|
137
137
|
workspace/product surface, and expose the HMAC key only to the supervisor identity.
|
|
138
138
|
**Also isolate code execution:** product-controlled behavioral checks must execute
|
|
139
139
|
under a separate, keyless OS/container identity, not merely as child processes of
|
|
140
|
-
that privileged supervisor.
|
|
140
|
+
that privileged supervisor. On Linux, an opt-in root-run mode
|
|
141
|
+
(`PLAN_AUDITOR_CHECK_ISOLATION=required` with distinct non-root
|
|
142
|
+
`PLAN_AUDITOR_CHECK_UID` and `PLAN_AUDITOR_CHECK_GID`) drops the subprocess
|
|
143
|
+
UID/GID and supplementary groups before running checks. The trusted supervisor
|
|
144
|
+
retains the HMAC key; the check cannot read its root-only key file or root-private
|
|
145
|
+
control state. The mode validates ownership and permissions and fails closed if
|
|
146
|
+
it cannot establish the boundary.
|
|
147
|
+
|
|
148
|
+
By default, or on Windows/macOS, the CLI still makes **no OS-isolation guarantee**.
|
|
149
|
+
UID/GID separation is not a full kernel sandbox: trusted installation paths,
|
|
150
|
+
host-owned configuration, capability/sudo restrictions, and optionally stronger
|
|
151
|
+
container/VM/network isolation remain the deployment operator's responsibility.
|
|
141
152
|
|
|
142
153
|
## Fail-closed terminology
|
|
143
154
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "plan-auditor"
|
|
7
|
-
version = "2.4.
|
|
7
|
+
version = "2.4.6"
|
|
8
8
|
description = "Deterministic verification and evidence auditing for AI coding agents: sealed plans, hash-chained evidence, and an independent completion gate"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -30,6 +30,7 @@ from contextlib import contextmanager
|
|
|
30
30
|
from typing import TYPE_CHECKING
|
|
31
31
|
|
|
32
32
|
if TYPE_CHECKING: # pragma: no cover - type checkers see the single package import
|
|
33
|
+
from scripts.check_isolation import check_launch_options
|
|
33
34
|
from scripts.contract import PlanTrustError
|
|
34
35
|
from scripts.exec_trust import require_plan_trust
|
|
35
36
|
else:
|
|
@@ -37,9 +38,11 @@ else:
|
|
|
37
38
|
# importable unprefixed. The runtime fallback is required; the TYPE_CHECKING
|
|
38
39
|
# branch keeps static analysis from seeing the names bound twice.
|
|
39
40
|
try:
|
|
41
|
+
from scripts.check_isolation import check_launch_options
|
|
40
42
|
from scripts.contract import PlanTrustError
|
|
41
43
|
from scripts.exec_trust import require_plan_trust
|
|
42
44
|
except ImportError:
|
|
45
|
+
from check_isolation import check_launch_options
|
|
43
46
|
from contract import PlanTrustError
|
|
44
47
|
from exec_trust import require_plan_trust
|
|
45
48
|
|
|
@@ -678,25 +681,20 @@ def _kill_process_tree(proc):
|
|
|
678
681
|
|
|
679
682
|
def _bounded_command(command, base, timeout, max_output):
|
|
680
683
|
"""Execute ``command`` (an argv list) with no shell and bounded resources."""
|
|
681
|
-
# Behavioral checks execute potentially untrusted project code. Do not pass
|
|
682
|
-
# the supervisor's authenticated-integrity credentials to these children.
|
|
683
|
-
# This is defense in depth, not an OS boundary: another process running as
|
|
684
|
-
# the same user may still read an externally stored key file.
|
|
685
|
-
child_env = os.environ.copy()
|
|
686
|
-
child_env.pop("PLAN_AUDITOR_HMAC_KEY", None)
|
|
687
|
-
child_env.pop("PLAN_AUDITOR_HMAC_KEY_FILE", None)
|
|
688
684
|
kwargs = {
|
|
689
685
|
"cwd": base,
|
|
690
686
|
"stdout": subprocess.PIPE,
|
|
691
687
|
"stderr": subprocess.STDOUT,
|
|
692
688
|
"shell": False,
|
|
693
|
-
"env": child_env,
|
|
694
689
|
}
|
|
695
690
|
if os.name == "nt":
|
|
696
691
|
kwargs["creationflags"] = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
|
|
697
692
|
else:
|
|
698
693
|
kwargs["start_new_session"] = True
|
|
699
694
|
try:
|
|
695
|
+
# Optional Linux root-run UID/GID drop. Even with no isolation enabled,
|
|
696
|
+
# never forward the HMAC key or key-file locator to behavioral checks.
|
|
697
|
+
kwargs.update(check_launch_options(base))
|
|
700
698
|
proc = subprocess.Popen(command, **kwargs)
|
|
701
699
|
except (OSError, ValueError) as exc:
|
|
702
700
|
return None, "start", str(exc), False
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Opt-in Linux privilege separation for project-controlled verification checks.
|
|
2
|
+
|
|
3
|
+
This is a UID/GID security boundary, not a container, seccomp or kernel sandbox.
|
|
4
|
+
A trusted root-owned supervisor and a root-owned key/control plane are required.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import stat
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
MODE_ENV = "PLAN_AUDITOR_CHECK_ISOLATION"
|
|
15
|
+
UID_ENV = "PLAN_AUDITOR_CHECK_UID"
|
|
16
|
+
GID_ENV = "PLAN_AUDITOR_CHECK_GID"
|
|
17
|
+
KEY_ENV = "PLAN_AUDITOR_HMAC_KEY"
|
|
18
|
+
KEY_FILE_ENV = "PLAN_AUDITOR_HMAC_KEY_FILE"
|
|
19
|
+
|
|
20
|
+
# The isolated child must not inherit arbitrary supervisor secrets.
|
|
21
|
+
_SAFE_ENV = ("PATH", "LANG", "LC_ALL", "LC_CTYPE", "TZ", "TERM", "PYTHONIOENCODING", "PYTHONUTF8")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CheckIsolationError(ValueError):
|
|
25
|
+
"""Refuse execution if the requested OS boundary cannot be established."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _positive_id(raw: str | None, name: str) -> int:
|
|
29
|
+
if not raw or not raw.isascii() or not raw.isdecimal():
|
|
30
|
+
raise CheckIsolationError(f"{name} must be a positive numeric Linux ID")
|
|
31
|
+
value = int(raw)
|
|
32
|
+
if not 0 < value < 2**31:
|
|
33
|
+
raise CheckIsolationError(f"{name} must be a non-root Linux ID")
|
|
34
|
+
return value
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _root_owned_private_control(root: Path) -> None:
|
|
38
|
+
workspace = root.resolve(strict=True)
|
|
39
|
+
# The agent must not be able to rename the workspace itself through a
|
|
40
|
+
# writable ancestor: private permissions on .plan-auditor alone are not enough.
|
|
41
|
+
for current in (workspace, *workspace.parents):
|
|
42
|
+
st = current.stat()
|
|
43
|
+
if st.st_uid != 0 or not stat.S_ISDIR(st.st_mode):
|
|
44
|
+
raise CheckIsolationError("workspace and ancestors must be root-owned directories")
|
|
45
|
+
if st.st_mode & 0o022 and not st.st_mode & stat.S_ISVTX:
|
|
46
|
+
raise CheckIsolationError(
|
|
47
|
+
"writable workspace/ancestor directories require root ownership and the sticky bit"
|
|
48
|
+
)
|
|
49
|
+
control = workspace / ".plan-auditor"
|
|
50
|
+
try:
|
|
51
|
+
control_stat = control.lstat()
|
|
52
|
+
except OSError as exc:
|
|
53
|
+
raise CheckIsolationError(f"cannot inspect trusted control directory: {exc}") from exc
|
|
54
|
+
if (not stat.S_ISDIR(control_stat.st_mode)
|
|
55
|
+
or control_stat.st_uid != 0 or control_stat.st_mode & 0o077):
|
|
56
|
+
raise CheckIsolationError(
|
|
57
|
+
"isolated checks require a root-owned, private (0700) .plan-auditor directory"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _private_root_key_file(root: Path) -> None:
|
|
62
|
+
value = os.environ.get(KEY_FILE_ENV)
|
|
63
|
+
if not value:
|
|
64
|
+
return
|
|
65
|
+
supplied = Path(value).expanduser()
|
|
66
|
+
if supplied.is_symlink():
|
|
67
|
+
raise CheckIsolationError("isolated checks reject HMAC key symlinks")
|
|
68
|
+
try:
|
|
69
|
+
path = supplied.resolve(strict=True)
|
|
70
|
+
info = path.stat()
|
|
71
|
+
except OSError as exc:
|
|
72
|
+
raise CheckIsolationError(f"cannot inspect HMAC key: {exc}") from exc
|
|
73
|
+
if path.is_relative_to(root):
|
|
74
|
+
raise CheckIsolationError("HMAC key must be outside the workspace")
|
|
75
|
+
if not stat.S_ISREG(info.st_mode) or info.st_uid != 0 or info.st_mode & 0o077:
|
|
76
|
+
raise CheckIsolationError("isolated checks require a root-owned 0600 HMAC key file")
|
|
77
|
+
# Neither the check user nor the agent should be able to replace the key
|
|
78
|
+
# by writing to one of its parent directories.
|
|
79
|
+
for parent in path.parents:
|
|
80
|
+
parent_stat = parent.stat()
|
|
81
|
+
if parent_stat.st_uid != 0 or parent_stat.st_mode & 0o022:
|
|
82
|
+
raise CheckIsolationError("HMAC key directory chain must be root-owned and non-writable")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def check_launch_options(base: str | os.PathLike[str]) -> dict[str, Any]:
|
|
86
|
+
"""Return safe subprocess options; fail closed on partial/invalid configuration."""
|
|
87
|
+
mode = os.environ.get(MODE_ENV)
|
|
88
|
+
uid_raw = os.environ.get(UID_ENV)
|
|
89
|
+
gid_raw = os.environ.get(GID_ENV)
|
|
90
|
+
child_env = os.environ.copy()
|
|
91
|
+
child_env.pop(KEY_ENV, None)
|
|
92
|
+
child_env.pop(KEY_FILE_ENV, None)
|
|
93
|
+
if mode is None and uid_raw is None and gid_raw is None:
|
|
94
|
+
return {"env": child_env}
|
|
95
|
+
if mode != "required":
|
|
96
|
+
raise CheckIsolationError(
|
|
97
|
+
"check identity settings require PLAN_AUDITOR_CHECK_ISOLATION=required"
|
|
98
|
+
)
|
|
99
|
+
if not sys.platform.startswith("linux") or not hasattr(os, "geteuid") or os.geteuid() != 0:
|
|
100
|
+
raise CheckIsolationError("required check isolation only supports a trusted root-run Linux verifier")
|
|
101
|
+
uid = _positive_id(uid_raw, UID_ENV)
|
|
102
|
+
gid = _positive_id(gid_raw, GID_ENV)
|
|
103
|
+
root = Path(base).resolve(strict=True)
|
|
104
|
+
_root_owned_private_control(root)
|
|
105
|
+
_private_root_key_file(root)
|
|
106
|
+
# Never pass through arbitrary environment variables such as API tokens,
|
|
107
|
+
# HOME, GITHUB_TOKEN, SSH_AUTH_SOCK or the verifier's Python import paths.
|
|
108
|
+
isolated_env = {k: os.environ[k] for k in _SAFE_ENV if k in os.environ}
|
|
109
|
+
isolated_env["HOME"] = "/nonexistent"
|
|
110
|
+
isolated_env["PYTHONDONTWRITEBYTECODE"] = "1"
|
|
111
|
+
return {
|
|
112
|
+
"env": isolated_env,
|
|
113
|
+
"user": uid,
|
|
114
|
+
"group": gid,
|
|
115
|
+
"extra_groups": [],
|
|
116
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Checks for the opt-in Linux root-supervisor / unprivileged-check boundary."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import stat
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from scripts import audit_check as core
|
|
14
|
+
from scripts.check_isolation import CheckIsolationError, check_launch_options
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _request_isolation(monkeypatch, uid="65534", gid="65534"):
|
|
18
|
+
monkeypatch.setenv("PLAN_AUDITOR_CHECK_ISOLATION", "required")
|
|
19
|
+
monkeypatch.setenv("PLAN_AUDITOR_CHECK_UID", uid)
|
|
20
|
+
monkeypatch.setenv("PLAN_AUDITOR_CHECK_GID", gid)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_partial_configuration_refuses_to_run(tmp_path, monkeypatch):
|
|
24
|
+
monkeypatch.setenv("PLAN_AUDITOR_CHECK_UID", "65534")
|
|
25
|
+
rc, state, reason, _overflow = core._bounded_command(
|
|
26
|
+
[sys.executable, "-c", "print('must not execute')"], str(tmp_path), 5, 2048
|
|
27
|
+
)
|
|
28
|
+
assert rc is None and state == "start"
|
|
29
|
+
assert "require PLAN_AUDITOR_CHECK_ISOLATION=required" in reason
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.parametrize("uid,gid", [("0", "65534"), ("65534", "0"), ("bad", "65534"), ("", "65534")])
|
|
33
|
+
def test_invalid_identity_fails_closed(tmp_path, monkeypatch, uid, gid):
|
|
34
|
+
_request_isolation(monkeypatch, uid=uid, gid=gid)
|
|
35
|
+
# Simulate a privileged host to inspect the identity validator on all OSes.
|
|
36
|
+
monkeypatch.setattr(sys, "platform", "linux")
|
|
37
|
+
monkeypatch.setattr(os, "geteuid", lambda: 0, raising=False)
|
|
38
|
+
with pytest.raises(CheckIsolationError, match="non-root|positive"):
|
|
39
|
+
check_launch_options(tmp_path)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_required_isolation_refuses_unprivileged_verifier(tmp_path, monkeypatch):
|
|
43
|
+
_request_isolation(monkeypatch)
|
|
44
|
+
monkeypatch.setattr(os, "geteuid", lambda: 1000, raising=False)
|
|
45
|
+
with pytest.raises(CheckIsolationError, match="root-run Linux"):
|
|
46
|
+
check_launch_options(tmp_path)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.skipif(not sys.platform.startswith("linux") or not hasattr(os, "geteuid") or os.geteuid() != 0,
|
|
50
|
+
reason="real kernel UID/GID isolation needs root-run Linux")
|
|
51
|
+
def test_real_unprivileged_check_cannot_read_hmac_or_control_plane(monkeypatch):
|
|
52
|
+
import pwd
|
|
53
|
+
|
|
54
|
+
check_user = pwd.getpwnam("nobody")
|
|
55
|
+
# root-owned sticky workspace allows agent-owned product files without
|
|
56
|
+
# permitting rename/replacement of root-owned .plan-auditor control state.
|
|
57
|
+
with tempfile.TemporaryDirectory(prefix="pa-os-check-", dir="/tmp") as folder, \
|
|
58
|
+
tempfile.TemporaryDirectory(prefix="pa-os-key-", dir="/root") as key_folder:
|
|
59
|
+
workspace = Path(folder)
|
|
60
|
+
workspace.chmod(0o1777)
|
|
61
|
+
control = workspace / ".plan-auditor"
|
|
62
|
+
control.mkdir(mode=0o700)
|
|
63
|
+
marker = control / "seal.json"
|
|
64
|
+
marker.write_text("trusted", encoding="utf-8")
|
|
65
|
+
key = Path(key_folder) / "secret.key"
|
|
66
|
+
key.write_text("not-a-real-credential", encoding="utf-8")
|
|
67
|
+
key.chmod(0o600)
|
|
68
|
+
|
|
69
|
+
_request_isolation(monkeypatch, str(check_user.pw_uid), str(check_user.pw_gid))
|
|
70
|
+
monkeypatch.setenv("PLAN_AUDITOR_HMAC_KEY_FILE", str(key))
|
|
71
|
+
monkeypatch.setenv("GITHUB_TOKEN", "do-not-inherit-test-token")
|
|
72
|
+
script = (
|
|
73
|
+
"import json,os,pathlib,sys;"
|
|
74
|
+
"key=pathlib.Path(sys.argv[1]);control=pathlib.Path(sys.argv[2]);"
|
|
75
|
+
"definitely_no_key=os.getenv('PLAN_AUDITOR_HMAC_KEY_FILE') is None;"
|
|
76
|
+
"denied=[];"
|
|
77
|
+
"\nfor p in (key,control):"
|
|
78
|
+
"\n try: p.read_bytes();denied.append(False)"
|
|
79
|
+
"\n except PermissionError: denied.append(True)"
|
|
80
|
+
"\nprint(json.dumps({'uid':os.geteuid(),'gid':os.getegid(),"
|
|
81
|
+
"'denied':denied,'key_hidden':definitely_no_key,"
|
|
82
|
+
"'other_secret_hidden':os.getenv('GITHUB_TOKEN') is None}))"
|
|
83
|
+
)
|
|
84
|
+
passed, detail, output = core.run_check(
|
|
85
|
+
{"type": "run", "argv": [sys.executable, "-c", script, str(key), str(marker)],
|
|
86
|
+
"expect_exit": 0, "output_regex": "key_hidden"},
|
|
87
|
+
str(workspace),
|
|
88
|
+
)
|
|
89
|
+
assert passed, (detail, output)
|
|
90
|
+
rc, state, raw, overflow = core._bounded_command(
|
|
91
|
+
[sys.executable, "-c", script, str(key), str(marker)], str(workspace), 15, 8192
|
|
92
|
+
)
|
|
93
|
+
assert (rc, state, overflow) == (0, "ok", False), raw
|
|
94
|
+
result = json.loads(raw.strip())
|
|
95
|
+
assert result == {
|
|
96
|
+
"uid": check_user.pw_uid, "gid": check_user.pw_gid,
|
|
97
|
+
"denied": [True, True], "key_hidden": True, "other_secret_hidden": True,
|
|
98
|
+
}
|
|
99
|
+
assert marker.read_text(encoding="utf-8") == "trusted"
|
|
100
|
+
assert stat.S_IMODE(key.stat().st_mode) == 0o600
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@pytest.mark.skipif(not sys.platform.startswith("linux") or not hasattr(os, "geteuid") or os.geteuid() != 0,
|
|
104
|
+
reason="real kernel filesystem policy needs root-run Linux")
|
|
105
|
+
def test_insecure_external_key_file_is_rejected(monkeypatch):
|
|
106
|
+
import pwd
|
|
107
|
+
|
|
108
|
+
user = pwd.getpwnam("nobody")
|
|
109
|
+
with tempfile.TemporaryDirectory(prefix="pa-os-check-", dir="/tmp") as folder, \
|
|
110
|
+
tempfile.TemporaryDirectory(prefix="pa-os-key-", dir="/root") as key_folder:
|
|
111
|
+
root = Path(folder)
|
|
112
|
+
root.chmod(0o1777)
|
|
113
|
+
(root / ".plan-auditor").mkdir(mode=0o700)
|
|
114
|
+
key = Path(key_folder) / "secret.key"
|
|
115
|
+
key.write_text("test-only-key", encoding="utf-8")
|
|
116
|
+
key.chmod(0o644)
|
|
117
|
+
_request_isolation(monkeypatch, str(user.pw_uid), str(user.pw_gid))
|
|
118
|
+
monkeypatch.setenv("PLAN_AUDITOR_HMAC_KEY_FILE", str(key))
|
|
119
|
+
with pytest.raises(CheckIsolationError, match="0600"):
|
|
120
|
+
check_launch_options(root)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@pytest.mark.skipif(not sys.platform.startswith("linux") or not hasattr(os, "geteuid") or os.geteuid() != 0,
|
|
124
|
+
reason="real Linux ownership checks need root")
|
|
125
|
+
def test_insecure_workspace_can_not_replace_trusted_control_state(monkeypatch):
|
|
126
|
+
import pwd
|
|
127
|
+
|
|
128
|
+
check_user = pwd.getpwnam("nobody")
|
|
129
|
+
with tempfile.TemporaryDirectory(prefix="pa-os-check-", dir="/tmp") as folder:
|
|
130
|
+
workspace = Path(folder)
|
|
131
|
+
workspace.chmod(0o777) # world writable without sticky bit
|
|
132
|
+
(workspace / ".plan-auditor").mkdir(mode=0o700)
|
|
133
|
+
_request_isolation(monkeypatch, str(check_user.pw_uid), str(check_user.pw_gid))
|
|
134
|
+
with pytest.raises(CheckIsolationError, match="sticky bit"):
|
|
135
|
+
check_launch_options(workspace)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@pytest.mark.skipif(not sys.platform.startswith("linux") or not hasattr(os, "geteuid") or os.geteuid() != 0,
|
|
139
|
+
reason="real Linux ownership checks need root")
|
|
140
|
+
def test_insecure_control_directory_is_rejected(monkeypatch):
|
|
141
|
+
import pwd
|
|
142
|
+
|
|
143
|
+
check_user = pwd.getpwnam("nobody")
|
|
144
|
+
with tempfile.TemporaryDirectory(prefix="pa-os-check-", dir="/tmp") as folder:
|
|
145
|
+
workspace = Path(folder)
|
|
146
|
+
workspace.chmod(0o1777)
|
|
147
|
+
(workspace / ".plan-auditor").mkdir(mode=0o755)
|
|
148
|
+
_request_isolation(monkeypatch, str(check_user.pw_uid), str(check_user.pw_gid))
|
|
149
|
+
with pytest.raises(CheckIsolationError, match="private"):
|
|
150
|
+
check_launch_options(workspace)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|