harness-everything 0.42.0 → 0.42.2
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +18 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/README.md +68 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/green-final.log +162 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/green.log +158 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/live-installed-runtime-rejection.json +31 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-action-family-cross-product.js +143 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-action-family-cross-product.log +7 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-codex-action-gate-prehook-smoke.js +78 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-codex-action-gate-prehook-smoke.log +8 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-review.md +151 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/original-rejection.json +31 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/red.log +158 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-descriptor-green.log +192 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-descriptor-red.log +192 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-family-green.log +206 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-family-red.log +206 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-hints-green.log +174 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-hints-red.log +174 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/verification.md +31 -0
- package/benchmarks/results/doc-evidence-e1/README.md +68 -0
- package/benchmarks/results/doc-evidence-e1/evidence.json +360 -0
- package/benchmarks/results/doc-evidence-e1/pilot-report.json +175 -0
- package/benchmarks/results/h1-provider-independence-qualification/README.md +26 -3
- package/benchmarks/results/live-host/opencode-d1-d2-2026-10-25/README.md +30 -0
- package/benchmarks/results/live-host/opencode-d1-d2-2026-10-25/evidence.json +128 -0
- package/ci/action-gate.test.js +86 -0
- package/ci/lib/doc-reconciliation-prep-fixtures.js +5 -4
- package/ci/mechanism-2aa-codex-action-gate-policy.test.js +47 -0
- package/ci/mechanism-325-3b-s2b-wiring.test.js +11 -0
- package/ci/mechanism-task-git-authorization.test.js +12 -0
- package/docs/codex-action-gate.md +27 -1
- package/docs/doc-workflow-reconciliation.md +4 -3
- package/docs/document-lifecycle.md +3 -3
- package/docs/platform-capabilities.md +8 -2
- package/hooks/scripts/action-gate.js +35 -8
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/action-gate.js +35 -8
- package/plugins/harness-everything/plugin.json +1 -1
- package/sbom.cdx.json +7 -7
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "harness-everything",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
|
|
13
|
-
"version": "0.42.
|
|
13
|
+
"version": "0.42.2",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Miya Daniel"
|
|
16
16
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harness-everything",
|
|
3
3
|
"description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
|
|
4
|
-
"version": "0.42.
|
|
4
|
+
"version": "0.42.2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Miya Daniel"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.42.2](https://github.com/dyphn1/Harness-everything/compare/v0.42.1...v0.42.2) (2026-10-26)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
* **test:** normalize macos private tmp paths in s2b replay ([55f82b6](https://github.com/dyphn1/Harness-everything/commit/55f82b64df046a7344fa1f60898a586719858e11))
|
|
10
|
+
|
|
11
|
+
## [0.42.1](https://github.com/dyphn1/Harness-everything/compare/v0.42.0...v0.42.1) (2026-10-26)
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **action-gate:** defer Codex PR title and body updates to host ([fd371bf](https://github.com/dyphn1/Harness-everything/commit/fd371bf16bbba7d5ecac80b18d7e3e0ee0144ed7))
|
|
16
|
+
* **action-gate:** honor required hints across conflicting envelopes ([ff796c9](https://github.com/dyphn1/Harness-everything/commit/ff796c9edda4565906ed4c0d2b0b14ed3ef6bbc0))
|
|
17
|
+
* **action-gate:** prevent codex pr mutation descriptor downgrades ([e9344de](https://github.com/dyphn1/Harness-everything/commit/e9344deb0ae38a6414944db51b2b2c40b2132c34))
|
|
18
|
+
* **action-gate:** retain codex pr tool identity during classification ([c170a96](https://github.com/dyphn1/Harness-everything/commit/c170a96cf856295200d2d1a62c15a552c3cb2b37))
|
|
19
|
+
|
|
5
20
|
## [0.42.0](https://github.com/dyphn1/Harness-everything/compare/v0.41.3...v0.42.0) (2026-10-25)
|
|
6
21
|
|
|
7
22
|
### Features
|
|
@@ -100,6 +115,9 @@ All notable changes to this project are documented in this file.
|
|
|
100
115
|
|
|
101
116
|
## [Unreleased]
|
|
102
117
|
|
|
118
|
+
### Fixed
|
|
119
|
+
- Codex PR title/body-only updates defer to native task scope and permissions after exact operation and argument checks; required Gate hints and high-risk fields remain gated.
|
|
120
|
+
|
|
103
121
|
- Keep successful installed workflow step dispositions from advancing verification freshness while unknown workflow commands and mutations remain tracked (#365).
|
|
104
122
|
|
|
105
123
|
- Accept an interior `~` in unquoted installed doc-check path tokens, and reject `%VAR%` expansion before identity and digest checks.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Codex PR metadata action-gate evidence
|
|
2
|
+
|
|
3
|
+
| Item | Evidence / disposition |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| Scope | Codex exact PR title/body update; no #376 governance changes, #341 E1 credit or #320 promotion |
|
|
6
|
+
| Base | `b733cadec17542b9ae30aabb50c1ba3481a58d74` (fresh `origin/main`, 2026-10-26) |
|
|
7
|
+
| RED | `db6c29b`; [red.log](red.log): 12 expected metadata failures, dependency/baseline tests PASS |
|
|
8
|
+
| GREEN | `fd371bf`; [green.log](green.log): all boundary checks PASS |
|
|
9
|
+
| Reproduce | `node ci/action-gate.test.js`; `node ci/mechanism-2aa-codex-action-gate-policy.test.js`; `node ci/mechanism-task-git-authorization.test.js` |
|
|
10
|
+
| Package | `npm run plugin:sync`; canonical/package action gate byte comparison and identical lifecycle tests |
|
|
11
|
+
| Final core | `c170a96cf856295200d2d1a62c15a552c3cb2b37` |
|
|
12
|
+
| Full regression | [Node 24 full run](verification.md): 13 PASS, 0 FAIL, 3 UNCHECKED (local Node 22 lanes); exit 0 |
|
|
13
|
+
| Reviewer routing | `spawn_agent`: `fork_turns=none`, `model=gpt-6-luna`, `reasoning_effort=xhigh`; host-issued model identity/effort attestation is not exposed |
|
|
14
|
+
| Independent review | [Luna cold review](luna-review.md): core PASS at `c170a96`; 1 BLOCKER + 2 MAJOR fixed and independently rechecked; no open code BLOCKER/MAJOR |
|
|
15
|
+
| Candidate live Host | UNCHECKED: current session loads immutable installed runtime `0.42.0-f51078676b56`, not this candidate; no hook/trust configuration changed |
|
|
16
|
+
|
|
17
|
+
## Original rejection provenance
|
|
18
|
+
|
|
19
|
+
- Observed 2026-10-26T01:12:15.045Z; session `01a175b3-4583-7590-9c9d-f71a9439ae8e`, tool call `exec-8e76e1be-e5a3-493d-807b-2ad6cdbc7c31`.
|
|
20
|
+
- Actual tool: `mcp__codex_apps__github__update_pull_request`; arguments: `repository_full_name`, `pr_number: 376`, `body` only.
|
|
21
|
+
- Audit: `structured-mutate`, `mutate`/`gated`, host `codex`, `rejected`, reason `host-ask-decision-unverified-or-unsupported`.
|
|
22
|
+
- Recomputed from the original transcript CommandExecution output at 2026-10-26T01:12:14.917Z (1,840 characters): MATCH. The temporary body file changed later and is not used as original evidence.
|
|
23
|
+
- Exact input SHA-256: `b9bda7116fb01c045edd71a6b4fc3091c0ab707e0b797f1b4efca6e7a4edc87b`. Audit intentionally omits raw body.
|
|
24
|
+
- Local audit: `~/.agents/harness-everything/workspaces/harness-everything-4534d9804d9c/state/sessions/01a175b3-4583-7590-9c9d-f71a9439ae8e/action-gate/exec-8e76e1be-e5a3-493d-807b-2ad6cdbc7c31.json`.
|
|
25
|
+
- Transcript: `~/.codex/sessions/2026/10/26/rollout-2026-10-26T08-54-05-01a175b3-4583-7590-9c9d-f71a9439ae8e.jsonl`, call `call_0a3ca81d5e134c76a9339a116e7fe417` supplies only those keys and records the matching Hook rejection.
|
|
26
|
+
- Current connected GitHub tool schema exposes those selectors, optional title/body and separate state/base_branch/maintainer_can_modify; new exception rejects the latter keys even when null.
|
|
27
|
+
|
|
28
|
+
## Boundary controls
|
|
29
|
+
|
|
30
|
+
- Body-only, title-only and combined payloads; exact bare/provider operation and both connected Codex naming forms.
|
|
31
|
+
- State/reopen/retarget/maintainer/unknown/mixed/malformed payloads and similar names retain Gate; all supported required-hint envelopes retain Gate.
|
|
32
|
+
- Other hosts retain existing structured rules. Create PR, merge, close, delete, deploy, publish, send and force-push controls remain unchanged.
|
|
33
|
+
- Canonical/package tests verify descriptor/hash, redacted audit provenance, PostToolUse success/failure, changed-payload integrity violation and rejected mutation outcomes.
|
|
34
|
+
- No actual destructive remote negative control is executed. Mechanism PASS is not live Host PASS.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## Independent review corrections and replay
|
|
38
|
+
|
|
39
|
+
| Finding | RED evidence | Fix / GREEN evidence |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| Earlier optional hint masked required hint | `1e77c26`, [9 failures](review-hints-red.log) | `ff796c9`, [PASS](review-hints-green.log) |
|
|
42
|
+
| Exact Codex PR mutation downgraded by read/unknown descriptor | `06a66c2`, [11 failures](review-descriptor-red.log) | `e9344de`, [PASS](review-descriptor-green.log) |
|
|
43
|
+
| MCP update disguised as shell family | `16f5e20`, [14 failures](review-family-red.log) | `c170a96`, [PASS](review-family-green.log) |
|
|
44
|
+
|
|
45
|
+
- Conflicting-hint precedence now preserves any required hint across all supported envelopes. Ordinary PR creation controls are unchanged; contradictory required hints honor the existing required-Gate contract.
|
|
46
|
+
- Exact-update descriptor/family hardening is Codex-only. Other hosts keep their prior descriptor behavior; higher-risk Codex descriptors remain gated.
|
|
47
|
+
- Initial RED/GREEN runs used Node 26.12.1 in the sibling-worktree shell; Luna independently replayed original RED under Node 24.22.1. Correction RED/GREEN and final regression use explicit Node 24 PATH.
|
|
48
|
+
- Earlier full runs were superseded, never claimed complete: Node 26 reached 128-suite self-regression PASS before stopping during mutations; the first Node 24 run stopped for the descriptor/family review correction.
|
|
49
|
+
|
|
50
|
+
Reproduce the independently authored checks from the repository root (Node 24):
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
node benchmarks/results/codex-pr-metadata-2026-10-26/luna-action-family-cross-product.js .
|
|
54
|
+
node benchmarks/results/codex-pr-metadata-2026-10-26/luna-codex-action-gate-prehook-smoke.js .
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- [Cross-product log](luna-action-family-cross-product.log): 2,709 cases, 4,729 assertions, 0 failures.
|
|
58
|
+
- [Stdin smoke log](luna-codex-action-gate-prehook-smoke.log): seven actual pre-hook subprocess cases PASS, covering host deferral and rejected cases with persisted audit fields.
|
|
59
|
+
- These are deterministic mechanism evidence, not live Codex Host evidence.
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
## Controlled GitHub delivery probe
|
|
63
|
+
|
|
64
|
+
- Draft [PR #377](https://github.com/dyphn1/Harness-everything/pull/377) was created via the normal GitHub MCP create path (no merge).
|
|
65
|
+
- The same body was then submitted via normal `github_update_pull_request`, with only repository_full_name/pr_number/body. The installed old runtime rejected it before execution as `structured-mutate`.
|
|
66
|
+
- [Preserved live rejection](live-installed-runtime-rejection.json) binds the attempted body to its payload hash and installed rule-table provenance. No alternate PR edit path was used after rejection; the PR body was not changed by this probe.
|
|
67
|
+
- This is live evidence of the installed old version's rejection, not candidate live PASS. Candidate update/title/body and comment lifecycle live-host execution remain UNCHECKED; no comment-operation exemption was added.
|
|
68
|
+
- CI: [initial PR workflow](https://github.com/dyphn1/Harness-everything/actions/runs/40275422862); final HEAD status must be checked separately after evidence commits.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
=== Issue #85 Phase 5 — Pre-action gate ===
|
|
2
|
+
PASS production action-gate rule table loads without fallback
|
|
3
|
+
PASS production rule table is non-empty and keeps the destructive baseline
|
|
4
|
+
PASS rule table contains git-force-push
|
|
5
|
+
PASS rule table contains git-reset-hard
|
|
6
|
+
PASS rule table contains git-clean-force-delete
|
|
7
|
+
PASS rule table contains recursive-delete
|
|
8
|
+
PASS rule table contains powershell-recursive-force-delete
|
|
9
|
+
PASS rule table contains sql-destructive-ddl
|
|
10
|
+
PASS rule table contains package-publish
|
|
11
|
+
PASS rule table contains github-release-create
|
|
12
|
+
PASS rule table contains production-deploy
|
|
13
|
+
PASS Claude force-push defers to the host permission flow without forcing a prompt
|
|
14
|
+
PASS deferred decision still names the matched rule
|
|
15
|
+
PASS matched Claude action is audited as deferred-to-host
|
|
16
|
+
PASS audit records matched rule
|
|
17
|
+
PASS audit hashes the exact command payload
|
|
18
|
+
PASS always-ask policy restores the forced Claude prompt
|
|
19
|
+
PASS ask decision uses Claude PreToolUse hookSpecificOutput
|
|
20
|
+
PASS ask reason names the matched rule
|
|
21
|
+
PASS always-ask action is audited as pending-approval
|
|
22
|
+
PASS git status passes untouched
|
|
23
|
+
PASS safe unmatched command creates no gate audit record
|
|
24
|
+
PASS permission_mode=default defers to the host without a forced prompt
|
|
25
|
+
PASS audit records permission_mode=default
|
|
26
|
+
PASS permission_mode=acceptEdits defers to the host without a forced prompt
|
|
27
|
+
PASS audit records permission_mode=acceptEdits
|
|
28
|
+
PASS permission_mode=plan defers to the host without a forced prompt
|
|
29
|
+
PASS audit records permission_mode=plan
|
|
30
|
+
PASS permission_mode=auto defers to the host without a forced prompt
|
|
31
|
+
PASS audit records permission_mode=auto
|
|
32
|
+
PASS permission_mode=dontAsk defers to the host without a forced prompt
|
|
33
|
+
PASS audit records permission_mode=dontAsk
|
|
34
|
+
PASS permission_mode=bypassPermissions defers to the host without a forced prompt
|
|
35
|
+
PASS audit records permission_mode=bypassPermissions
|
|
36
|
+
PASS permission_mode=missing defers to the host without a forced prompt
|
|
37
|
+
PASS audit records permission_mode=missing
|
|
38
|
+
PASS command classification gates destructive action even when router hint says not required
|
|
39
|
+
PASS router actionGate hint is recorded without controlling enforcement
|
|
40
|
+
PASS safe command is not gated solely because prompt router predicted risk
|
|
41
|
+
PASS structured MCP delete is gated before execution
|
|
42
|
+
PASS audit retains normalized structured action identity
|
|
43
|
+
PASS structured action audit binds to exact payload hash
|
|
44
|
+
PASS structured action audit does not persist raw secret arguments
|
|
45
|
+
PASS read-only MCP fixture passes without a false gate
|
|
46
|
+
PASS obvious structured mutation is classified from tool identity
|
|
47
|
+
PASS non-shell local-function destructive fixture uses the same structured gate
|
|
48
|
+
PASS unknown structured action fails closed when workflow requires a gate
|
|
49
|
+
PASS unknown structured action is not fabricated as destructive without a gate requirement
|
|
50
|
+
PASS changed structured payload cannot reuse a prior approval/audit decision
|
|
51
|
+
PASS PowerShell recursive force delete outside scratch is gated
|
|
52
|
+
PASS PowerShell recursive force delete inside session scratch is exempt
|
|
53
|
+
PASS rm -rf outside scratch is gated
|
|
54
|
+
PASS rm -rf inside OS temp is exempt
|
|
55
|
+
PASS git reset --hard HEAD matches git-reset-hard
|
|
56
|
+
PASS git clean -fd matches git-clean-force-delete
|
|
57
|
+
PASS psql -c "DROP TABLE users" matches sql-destructive-ddl
|
|
58
|
+
PASS npm publish matches package-publish
|
|
59
|
+
PASS gh release create v1.2.3 matches github-release-create
|
|
60
|
+
PASS npm run deploy matches production-deploy
|
|
61
|
+
PASS host without verified ask-style decision blocks with exit 2
|
|
62
|
+
PASS non-ask host block reason names the matched rule
|
|
63
|
+
PASS non-ask host block is audited as rejected
|
|
64
|
+
PASS internal action-gate error on Claude defers to the host instead of allowing or forcing a prompt
|
|
65
|
+
PASS internal error is shown to the user as a systemMessage without a permission decision
|
|
66
|
+
PASS internal error on Claude is audited as deferred-to-host
|
|
67
|
+
PASS internal action-gate error under always-ask fails closed to ask
|
|
68
|
+
PASS internal error on host without ask fails closed to block
|
|
69
|
+
PASS missing/invalid rule table visibly falls back to built-in defaults
|
|
70
|
+
PASS built-in fallback still classifies and audits destructive action
|
|
71
|
+
PASS fallback degradation is shown to the user as a systemMessage
|
|
72
|
+
PASS valid empty rule table does not silently activate built-in defaults
|
|
73
|
+
PASS empty-table mutation control disables detection, proving production non-empty assertion is meaningful
|
|
74
|
+
PASS null-prototype serialization preserves an own __proto__ key
|
|
75
|
+
PASS own __proto__ key changes the full-input digest
|
|
76
|
+
PASS PostToolUse closes a deferred payload as executed
|
|
77
|
+
PASS executed audit keeps exact approved payload hash
|
|
78
|
+
PASS PostToolUseFailure records attempted execution without pretending success
|
|
79
|
+
PASS post-tool payload mismatch is never accepted as the approved command
|
|
80
|
+
PASS Stop closes unexecuted deferred and pending payloads
|
|
81
|
+
PASS unexecuted deferred payload is audited as rejected
|
|
82
|
+
PASS unexecuted always-ask approval is audited as rejected
|
|
83
|
+
PASS hook process returns no decision for a matched command under the default policy
|
|
84
|
+
PASS hook process audits the deferred command with its permission mode
|
|
85
|
+
PASS HARNESS_ACTION_GATE_POLICY=always-ask makes the hook process emit ask
|
|
86
|
+
PASS Claude hook manifest wires action-gate to shell and MCP pre-action surfaces
|
|
87
|
+
PASS Claude hook manifest audits successful tool execution
|
|
88
|
+
PASS Claude hook manifest audits failed tool execution
|
|
89
|
+
PASS Claude hook manifest closes unresolved approvals on Stop
|
|
90
|
+
PASS OpenAI plugin preserves the attribution-aware Codex shell action-gate adapter
|
|
91
|
+
PASS OpenAI plugin packages an isolated structured MCP pre-action adapter without widening unrelated hooks
|
|
92
|
+
PASS OpenAI plugin packages an isolated structured MCP post-action audit adapter
|
|
93
|
+
PASS Codex plugin does not declare unsupported PostToolUseFailure lifecycle event
|
|
94
|
+
PASS Codex plugin infers action-gate outcome from supported PostToolUse
|
|
95
|
+
PASS Codex PostToolUse maps zero exit code to success
|
|
96
|
+
PASS Codex PostToolUse maps non-zero exit code to failure
|
|
97
|
+
PASS Codex PostToolUse conservatively recognizes failure stderr without exit code
|
|
98
|
+
PASS OpenAI plugin closes pending approvals at Stop
|
|
99
|
+
PASS canonical and plugin action-gate rule tables are byte-identical
|
|
100
|
+
PASS OpenAI plugin packages action-gate runtime
|
|
101
|
+
PASS update_pull_request body defers without a permission decision
|
|
102
|
+
PASS metadata remains a non-safe mutation
|
|
103
|
+
PASS update_pull_request title defers without a permission decision
|
|
104
|
+
PASS metadata remains a non-safe mutation
|
|
105
|
+
PASS update_pull_request title,body defers without a permission decision
|
|
106
|
+
PASS metadata remains a non-safe mutation
|
|
107
|
+
PASS mcp__github__update_pull_request body defers without a permission decision
|
|
108
|
+
PASS metadata remains a non-safe mutation
|
|
109
|
+
PASS mcp__github__update_pull_request title defers without a permission decision
|
|
110
|
+
PASS metadata remains a non-safe mutation
|
|
111
|
+
PASS mcp__github__update_pull_request title,body defers without a permission decision
|
|
112
|
+
PASS metadata remains a non-safe mutation
|
|
113
|
+
PASS mcp__codex_apps__github__update_pull_request body defers without a permission decision
|
|
114
|
+
PASS metadata remains a non-safe mutation
|
|
115
|
+
PASS mcp__codex_apps__github__update_pull_request title defers without a permission decision
|
|
116
|
+
PASS metadata remains a non-safe mutation
|
|
117
|
+
PASS mcp__codex_apps__github__update_pull_request title,body defers without a permission decision
|
|
118
|
+
PASS metadata remains a non-safe mutation
|
|
119
|
+
PASS mcp__codex_apps__github_update_pull_request body defers without a permission decision
|
|
120
|
+
PASS metadata remains a non-safe mutation
|
|
121
|
+
PASS mcp__codex_apps__github_update_pull_request title defers without a permission decision
|
|
122
|
+
PASS metadata remains a non-safe mutation
|
|
123
|
+
PASS mcp__codex_apps__github_update_pull_request title,body defers without a permission decision
|
|
124
|
+
PASS metadata remains a non-safe mutation
|
|
125
|
+
PASS provider-neutral selectors support metadata updates
|
|
126
|
+
PASS provider-neutral selectors support metadata updates
|
|
127
|
+
PASS own __proto__ field cannot escape the argument allowlist
|
|
128
|
+
PASS explicit higher-risk action cannot inherit metadata deferral
|
|
129
|
+
PASS metadata with {"state":"closed"} remains gated
|
|
130
|
+
PASS metadata with {"state":"open"} remains gated
|
|
131
|
+
PASS metadata with {"state":null} remains gated
|
|
132
|
+
PASS metadata with {"base_branch":"other"} remains gated
|
|
133
|
+
PASS metadata with {"maintainer_can_modify":false} remains gated
|
|
134
|
+
PASS metadata with {"maintainer_can_modify":null} remains gated
|
|
135
|
+
PASS metadata with {"unknown":true} remains gated
|
|
136
|
+
PASS metadata with {"title":"mixed","state":"closed"} remains gated
|
|
137
|
+
PASS metadata with {"draft":true} remains gated
|
|
138
|
+
PASS metadata with {"base":"other"} remains gated
|
|
139
|
+
PASS metadata with {"action_descriptor":{"action":"delete"}} remains gated
|
|
140
|
+
PASS missing or malformed metadata remains gated
|
|
141
|
+
PASS missing or malformed metadata remains gated
|
|
142
|
+
PASS missing or malformed metadata remains gated
|
|
143
|
+
PASS missing or malformed metadata remains gated
|
|
144
|
+
PASS missing or malformed metadata remains gated
|
|
145
|
+
PASS missing or malformed metadata remains gated
|
|
146
|
+
PASS missing or malformed metadata remains gated
|
|
147
|
+
PASS missing or malformed metadata remains gated
|
|
148
|
+
PASS mcp__demo__update_pull_requests receives no metadata exception
|
|
149
|
+
PASS mcp__demo__foo_update_pull_request receives no metadata exception
|
|
150
|
+
PASS mcp__demo__update_pull_request_and_delete receives no metadata exception
|
|
151
|
+
PASS mcp__codex_apps__github_update_pull_request_extra receives no metadata exception
|
|
152
|
+
PASS mcp__demo__merge_pull_request receives no metadata exception
|
|
153
|
+
PASS mcp__demo__close_pull_request receives no metadata exception
|
|
154
|
+
PASS claude retains existing mutation rule
|
|
155
|
+
PASS opencode retains existing mutation rule
|
|
156
|
+
PASS unknown retains existing mutation rule
|
|
157
|
+
PASS explicit required metadata gate cannot be bypassed
|
|
158
|
+
PASS explicit required metadata gate cannot be bypassed
|
|
159
|
+
PASS explicit required metadata gate cannot be bypassed
|
|
160
|
+
PASS explicit required metadata gate cannot be bypassed
|
|
161
|
+
|
|
162
|
+
PASS: action-gate phase 5 (0 failures)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
=== Issue #85 Phase 5 — Pre-action gate ===
|
|
2
|
+
PASS production action-gate rule table loads without fallback
|
|
3
|
+
PASS production rule table is non-empty and keeps the destructive baseline
|
|
4
|
+
PASS rule table contains git-force-push
|
|
5
|
+
PASS rule table contains git-reset-hard
|
|
6
|
+
PASS rule table contains git-clean-force-delete
|
|
7
|
+
PASS rule table contains recursive-delete
|
|
8
|
+
PASS rule table contains powershell-recursive-force-delete
|
|
9
|
+
PASS rule table contains sql-destructive-ddl
|
|
10
|
+
PASS rule table contains package-publish
|
|
11
|
+
PASS rule table contains github-release-create
|
|
12
|
+
PASS rule table contains production-deploy
|
|
13
|
+
PASS Claude force-push defers to the host permission flow without forcing a prompt
|
|
14
|
+
PASS deferred decision still names the matched rule
|
|
15
|
+
PASS matched Claude action is audited as deferred-to-host
|
|
16
|
+
PASS audit records matched rule
|
|
17
|
+
PASS audit hashes the exact command payload
|
|
18
|
+
PASS always-ask policy restores the forced Claude prompt
|
|
19
|
+
PASS ask decision uses Claude PreToolUse hookSpecificOutput
|
|
20
|
+
PASS ask reason names the matched rule
|
|
21
|
+
PASS always-ask action is audited as pending-approval
|
|
22
|
+
PASS git status passes untouched
|
|
23
|
+
PASS safe unmatched command creates no gate audit record
|
|
24
|
+
PASS permission_mode=default defers to the host without a forced prompt
|
|
25
|
+
PASS audit records permission_mode=default
|
|
26
|
+
PASS permission_mode=acceptEdits defers to the host without a forced prompt
|
|
27
|
+
PASS audit records permission_mode=acceptEdits
|
|
28
|
+
PASS permission_mode=plan defers to the host without a forced prompt
|
|
29
|
+
PASS audit records permission_mode=plan
|
|
30
|
+
PASS permission_mode=auto defers to the host without a forced prompt
|
|
31
|
+
PASS audit records permission_mode=auto
|
|
32
|
+
PASS permission_mode=dontAsk defers to the host without a forced prompt
|
|
33
|
+
PASS audit records permission_mode=dontAsk
|
|
34
|
+
PASS permission_mode=bypassPermissions defers to the host without a forced prompt
|
|
35
|
+
PASS audit records permission_mode=bypassPermissions
|
|
36
|
+
PASS permission_mode=missing defers to the host without a forced prompt
|
|
37
|
+
PASS audit records permission_mode=missing
|
|
38
|
+
PASS command classification gates destructive action even when router hint says not required
|
|
39
|
+
PASS router actionGate hint is recorded without controlling enforcement
|
|
40
|
+
PASS safe command is not gated solely because prompt router predicted risk
|
|
41
|
+
PASS structured MCP delete is gated before execution
|
|
42
|
+
PASS audit retains normalized structured action identity
|
|
43
|
+
PASS structured action audit binds to exact payload hash
|
|
44
|
+
PASS structured action audit does not persist raw secret arguments
|
|
45
|
+
PASS read-only MCP fixture passes without a false gate
|
|
46
|
+
PASS obvious structured mutation is classified from tool identity
|
|
47
|
+
PASS non-shell local-function destructive fixture uses the same structured gate
|
|
48
|
+
PASS unknown structured action fails closed when workflow requires a gate
|
|
49
|
+
PASS unknown structured action is not fabricated as destructive without a gate requirement
|
|
50
|
+
PASS changed structured payload cannot reuse a prior approval/audit decision
|
|
51
|
+
PASS PowerShell recursive force delete outside scratch is gated
|
|
52
|
+
PASS PowerShell recursive force delete inside session scratch is exempt
|
|
53
|
+
PASS rm -rf outside scratch is gated
|
|
54
|
+
PASS rm -rf inside OS temp is exempt
|
|
55
|
+
PASS git reset --hard HEAD matches git-reset-hard
|
|
56
|
+
PASS git clean -fd matches git-clean-force-delete
|
|
57
|
+
PASS psql -c "DROP TABLE users" matches sql-destructive-ddl
|
|
58
|
+
PASS npm publish matches package-publish
|
|
59
|
+
PASS gh release create v1.2.3 matches github-release-create
|
|
60
|
+
PASS npm run deploy matches production-deploy
|
|
61
|
+
PASS host without verified ask-style decision blocks with exit 2
|
|
62
|
+
PASS non-ask host block reason names the matched rule
|
|
63
|
+
PASS non-ask host block is audited as rejected
|
|
64
|
+
PASS internal action-gate error on Claude defers to the host instead of allowing or forcing a prompt
|
|
65
|
+
PASS internal error is shown to the user as a systemMessage without a permission decision
|
|
66
|
+
PASS internal error on Claude is audited as deferred-to-host
|
|
67
|
+
PASS internal action-gate error under always-ask fails closed to ask
|
|
68
|
+
PASS internal error on host without ask fails closed to block
|
|
69
|
+
PASS missing/invalid rule table visibly falls back to built-in defaults
|
|
70
|
+
PASS built-in fallback still classifies and audits destructive action
|
|
71
|
+
PASS fallback degradation is shown to the user as a systemMessage
|
|
72
|
+
PASS valid empty rule table does not silently activate built-in defaults
|
|
73
|
+
PASS empty-table mutation control disables detection, proving production non-empty assertion is meaningful
|
|
74
|
+
PASS null-prototype serialization preserves an own __proto__ key
|
|
75
|
+
PASS own __proto__ key changes the full-input digest
|
|
76
|
+
PASS PostToolUse closes a deferred payload as executed
|
|
77
|
+
PASS executed audit keeps exact approved payload hash
|
|
78
|
+
PASS PostToolUseFailure records attempted execution without pretending success
|
|
79
|
+
PASS post-tool payload mismatch is never accepted as the approved command
|
|
80
|
+
PASS Stop closes unexecuted deferred and pending payloads
|
|
81
|
+
PASS unexecuted deferred payload is audited as rejected
|
|
82
|
+
PASS unexecuted always-ask approval is audited as rejected
|
|
83
|
+
PASS hook process returns no decision for a matched command under the default policy
|
|
84
|
+
PASS hook process audits the deferred command with its permission mode
|
|
85
|
+
PASS HARNESS_ACTION_GATE_POLICY=always-ask makes the hook process emit ask
|
|
86
|
+
PASS Claude hook manifest wires action-gate to shell and MCP pre-action surfaces
|
|
87
|
+
PASS Claude hook manifest audits successful tool execution
|
|
88
|
+
PASS Claude hook manifest audits failed tool execution
|
|
89
|
+
PASS Claude hook manifest closes unresolved approvals on Stop
|
|
90
|
+
PASS OpenAI plugin preserves the attribution-aware Codex shell action-gate adapter
|
|
91
|
+
PASS OpenAI plugin packages an isolated structured MCP pre-action adapter without widening unrelated hooks
|
|
92
|
+
PASS OpenAI plugin packages an isolated structured MCP post-action audit adapter
|
|
93
|
+
PASS Codex plugin does not declare unsupported PostToolUseFailure lifecycle event
|
|
94
|
+
PASS Codex plugin infers action-gate outcome from supported PostToolUse
|
|
95
|
+
PASS Codex PostToolUse maps zero exit code to success
|
|
96
|
+
PASS Codex PostToolUse maps non-zero exit code to failure
|
|
97
|
+
PASS Codex PostToolUse conservatively recognizes failure stderr without exit code
|
|
98
|
+
PASS OpenAI plugin closes pending approvals at Stop
|
|
99
|
+
PASS canonical and plugin action-gate rule tables are byte-identical
|
|
100
|
+
PASS OpenAI plugin packages action-gate runtime
|
|
101
|
+
PASS update_pull_request body defers without a permission decision
|
|
102
|
+
PASS metadata remains a non-safe mutation
|
|
103
|
+
PASS update_pull_request title defers without a permission decision
|
|
104
|
+
PASS metadata remains a non-safe mutation
|
|
105
|
+
PASS update_pull_request title,body defers without a permission decision
|
|
106
|
+
PASS metadata remains a non-safe mutation
|
|
107
|
+
PASS mcp__github__update_pull_request body defers without a permission decision
|
|
108
|
+
PASS metadata remains a non-safe mutation
|
|
109
|
+
PASS mcp__github__update_pull_request title defers without a permission decision
|
|
110
|
+
PASS metadata remains a non-safe mutation
|
|
111
|
+
PASS mcp__github__update_pull_request title,body defers without a permission decision
|
|
112
|
+
PASS metadata remains a non-safe mutation
|
|
113
|
+
PASS mcp__codex_apps__github__update_pull_request body defers without a permission decision
|
|
114
|
+
PASS metadata remains a non-safe mutation
|
|
115
|
+
PASS mcp__codex_apps__github__update_pull_request title defers without a permission decision
|
|
116
|
+
PASS metadata remains a non-safe mutation
|
|
117
|
+
PASS mcp__codex_apps__github__update_pull_request title,body defers without a permission decision
|
|
118
|
+
PASS metadata remains a non-safe mutation
|
|
119
|
+
PASS mcp__codex_apps__github_update_pull_request body defers without a permission decision
|
|
120
|
+
PASS metadata remains a non-safe mutation
|
|
121
|
+
PASS mcp__codex_apps__github_update_pull_request title defers without a permission decision
|
|
122
|
+
PASS metadata remains a non-safe mutation
|
|
123
|
+
PASS mcp__codex_apps__github_update_pull_request title,body defers without a permission decision
|
|
124
|
+
PASS metadata remains a non-safe mutation
|
|
125
|
+
PASS metadata with {"state":"closed"} remains gated
|
|
126
|
+
PASS metadata with {"state":"open"} remains gated
|
|
127
|
+
PASS metadata with {"state":null} remains gated
|
|
128
|
+
PASS metadata with {"base_branch":"other"} remains gated
|
|
129
|
+
PASS metadata with {"maintainer_can_modify":false} remains gated
|
|
130
|
+
PASS metadata with {"maintainer_can_modify":null} remains gated
|
|
131
|
+
PASS metadata with {"unknown":true} remains gated
|
|
132
|
+
PASS metadata with {"title":"mixed","state":"closed"} remains gated
|
|
133
|
+
PASS metadata with {"draft":true} remains gated
|
|
134
|
+
PASS metadata with {"base":"other"} remains gated
|
|
135
|
+
PASS metadata with {"action_descriptor":{"action":"delete"}} remains gated
|
|
136
|
+
PASS missing or malformed metadata remains gated
|
|
137
|
+
PASS missing or malformed metadata remains gated
|
|
138
|
+
PASS missing or malformed metadata remains gated
|
|
139
|
+
PASS missing or malformed metadata remains gated
|
|
140
|
+
PASS missing or malformed metadata remains gated
|
|
141
|
+
PASS missing or malformed metadata remains gated
|
|
142
|
+
PASS missing or malformed metadata remains gated
|
|
143
|
+
PASS missing or malformed metadata remains gated
|
|
144
|
+
PASS mcp__demo__update_pull_requests receives no metadata exception
|
|
145
|
+
PASS mcp__demo__foo_update_pull_request receives no metadata exception
|
|
146
|
+
PASS mcp__demo__update_pull_request_and_delete receives no metadata exception
|
|
147
|
+
PASS mcp__codex_apps__github_update_pull_request_extra receives no metadata exception
|
|
148
|
+
PASS mcp__demo__merge_pull_request receives no metadata exception
|
|
149
|
+
PASS mcp__demo__close_pull_request receives no metadata exception
|
|
150
|
+
PASS claude retains existing mutation rule
|
|
151
|
+
PASS opencode retains existing mutation rule
|
|
152
|
+
PASS unknown retains existing mutation rule
|
|
153
|
+
PASS explicit required metadata gate cannot be bypassed
|
|
154
|
+
PASS explicit required metadata gate cannot be bypassed
|
|
155
|
+
PASS explicit required metadata gate cannot be bypassed
|
|
156
|
+
PASS explicit required metadata gate cannot be bypassed
|
|
157
|
+
|
|
158
|
+
PASS: action-gate phase 5 (0 failures)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"toolName": "mcp__codex_apps__github__update_pull_request",
|
|
4
|
+
"toolUseId": "exec-77d1176e-229f-4453-b63a-93b42534a096",
|
|
5
|
+
"commandHash": "f31fc211c0806cea786a4746147685d783325fd765a3baef3ee04157b625a73e",
|
|
6
|
+
"actionDescriptor": {
|
|
7
|
+
"schemaVersion": 1,
|
|
8
|
+
"toolFamily": "mcp",
|
|
9
|
+
"toolName": "mcp__codex_apps__github__update_pull_request",
|
|
10
|
+
"action": "mutate",
|
|
11
|
+
"resource": "dyphn1/Harness-everything",
|
|
12
|
+
"payloadHash": "f31fc211c0806cea786a4746147685d783325fd765a3baef3ee04157b625a73e",
|
|
13
|
+
"classification": "gated",
|
|
14
|
+
"reasonCodes": [
|
|
15
|
+
"tool-identity-inference"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"matchedRule": "structured-mutate",
|
|
19
|
+
"ruleReason": "Structured mutate action requires pre-action authorization.",
|
|
20
|
+
"disposition": "rejected",
|
|
21
|
+
"host": "codex",
|
|
22
|
+
"permissionMode": "bypassPermissions",
|
|
23
|
+
"gatePolicy": "defer-to-host",
|
|
24
|
+
"routerActionGate": null,
|
|
25
|
+
"ruleTableSource": "/Users/daniel.chang/.codex/harness-everything/compat-hooks/runtime/0.42.0-f51078676b56/hooks/scripts/action-gate-rules.json",
|
|
26
|
+
"degradedRuleTable": false,
|
|
27
|
+
"degradationReason": null,
|
|
28
|
+
"createdAt": "2026-10-26T05:39:48.827Z",
|
|
29
|
+
"updatedAt": "2026-10-26T05:39:48.827Z",
|
|
30
|
+
"rejectionReason": "host-ask-decision-unverified-or-unsupported"
|
|
31
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const repoRoot = path.resolve(process.argv[2] || process.cwd());
|
|
6
|
+
const gate = require(path.join(repoRoot, 'hooks/scripts/action-gate.js'));
|
|
7
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'luna-action-cross-'));
|
|
8
|
+
const table = gate.loadRuleTable(path.join(repoRoot, 'hooks/scripts/action-gate-rules.json'));
|
|
9
|
+
const families = [undefined, 'mcp', 'local-function', 'shell', 'bogus'];
|
|
10
|
+
const actions = [undefined, 'read', 'unknown', 'mutate', 'delete', 'publish', 'deploy', 'send', 'bogus'];
|
|
11
|
+
const envelopes = [
|
|
12
|
+
['payload.action_descriptor', d => ({ action_descriptor: d })],
|
|
13
|
+
['payload.actionDescriptor', d => ({ actionDescriptor: d })],
|
|
14
|
+
['tool_input.action_descriptor', d => ({ tool_input: { ...baseInput, action_descriptor: d } })],
|
|
15
|
+
['tool_input.actionDescriptor', d => ({ tool_input: { ...baseInput, actionDescriptor: d } })],
|
|
16
|
+
['harness.action_descriptor', d => ({ harness: { action_descriptor: d } })],
|
|
17
|
+
['harness.actionDescriptor', d => ({ harness: { actionDescriptor: d } })],
|
|
18
|
+
];
|
|
19
|
+
const hintEnvelopes = [
|
|
20
|
+
gateValue => ({ actionGate: gateValue }),
|
|
21
|
+
gateValue => ({ workflowPlan: { actionGate: gateValue } }),
|
|
22
|
+
gateValue => ({ routerContract: { workflowPlan: { actionGate: gateValue } } }),
|
|
23
|
+
gateValue => ({ harness: { workflowPlan: { actionGate: gateValue } } }),
|
|
24
|
+
];
|
|
25
|
+
const unsafeFields = [
|
|
26
|
+
{ state: null },
|
|
27
|
+
{ base_branch: null },
|
|
28
|
+
{ maintainer_can_modify: null },
|
|
29
|
+
{ unknown_future_flag: null },
|
|
30
|
+
{ draft: null },
|
|
31
|
+
];
|
|
32
|
+
const baseInput = { repository_full_name: 'acme/repo', pr_number: 376, body: 'fixture' };
|
|
33
|
+
let sequence = 0;
|
|
34
|
+
let assertions = 0;
|
|
35
|
+
const failures = [];
|
|
36
|
+
function check(ok, label, actual) {
|
|
37
|
+
assertions++;
|
|
38
|
+
if (!ok && failures.length < 20) failures.push({ label, actual });
|
|
39
|
+
}
|
|
40
|
+
function merge(a, b) {
|
|
41
|
+
const out = { ...a };
|
|
42
|
+
for (const [key, value] of Object.entries(b || {})) {
|
|
43
|
+
const left = out[key];
|
|
44
|
+
if (left && value && typeof left === 'object' && typeof value === 'object' && !Array.isArray(left) && !Array.isArray(value)) out[key] = merge(left, value);
|
|
45
|
+
else out[key] = value;
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
function candidate(envelopeBuilder, descriptor, extra = {}) {
|
|
50
|
+
let payload = {
|
|
51
|
+
hook_event_name: 'PreToolUse',
|
|
52
|
+
session_id: 'cross-product',
|
|
53
|
+
tool_use_id: `cross-${sequence++}`,
|
|
54
|
+
tool_name: 'mcp__codex_apps__github__update_pull_request',
|
|
55
|
+
tool_input: { ...baseInput },
|
|
56
|
+
};
|
|
57
|
+
payload = merge(payload, extra);
|
|
58
|
+
if (envelopeBuilder) payload = merge(payload, envelopeBuilder(descriptor));
|
|
59
|
+
return payload;
|
|
60
|
+
}
|
|
61
|
+
function evaluate(payload) {
|
|
62
|
+
return gate.evaluatePreToolUse(payload, {
|
|
63
|
+
host: 'codex',
|
|
64
|
+
ruleTable: table,
|
|
65
|
+
sessionDir: path.join(root, 'session'),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function descriptorFor(action, family) {
|
|
69
|
+
const d = {};
|
|
70
|
+
if (action !== undefined) d.action = action;
|
|
71
|
+
if (family !== undefined) d.toolFamily = family;
|
|
72
|
+
return d;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
// Valid metadata arguments: every descriptor location/family/action combination.
|
|
76
|
+
for (const [envelopeName, build] of envelopes) {
|
|
77
|
+
for (const family of families) {
|
|
78
|
+
for (const action of actions) {
|
|
79
|
+
const p = candidate(build, descriptorFor(action, family));
|
|
80
|
+
const result = evaluate(p);
|
|
81
|
+
const expectedAction = ['delete', 'publish', 'deploy', 'send'].includes(action) ? action : 'mutate';
|
|
82
|
+
const expectedDisposition = ['delete', 'publish', 'deploy', 'send'].includes(action) || envelopeName.startsWith('tool_input.') ? 'block' : 'allow';
|
|
83
|
+
check(result.kind === expectedDisposition, `metadata action/family/envelope ${String(action)}/${String(family)}/${envelopeName}`, { kind: result.kind, rule: result.rule?.id });
|
|
84
|
+
if (result.record) {
|
|
85
|
+
check(result.record.actionDescriptor.action === expectedAction, `decision descriptor action ${String(action)}/${String(family)}/${envelopeName}`, { action: result.record.actionDescriptor.action });
|
|
86
|
+
check(result.record.actionDescriptor.toolFamily === 'mcp', `decision descriptor family ${String(action)}/${String(family)}/${envelopeName}`, { family: result.record.actionDescriptor.toolFamily });
|
|
87
|
+
}
|
|
88
|
+
if (expectedDisposition === 'allow') {
|
|
89
|
+
check(result.record?.disposition === 'deferred-to-host', `metadata audit ${String(action)}/${String(family)}/${envelopeName}`, { record: result.record?.disposition });
|
|
90
|
+
check(result.record?.actionDescriptor.action === expectedAction, `metadata descriptor action ${String(action)}/${String(family)}/${envelopeName}`, { action: result.record?.actionDescriptor.action });
|
|
91
|
+
check(result.record?.actionDescriptor.classification === 'gated', `metadata descriptor class ${String(action)}/${String(family)}/${envelopeName}`, { classification: result.record?.actionDescriptor.classification });
|
|
92
|
+
check(result.record?.actionDescriptor.toolFamily === 'mcp', `metadata descriptor family ${String(action)}/${String(family)}/${envelopeName}`, { family: result.record?.actionDescriptor.toolFamily });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// All four required hint envelopes must win over all optional envelopes for every action/family/descriptor source.
|
|
98
|
+
for (const [envelopeName, build] of envelopes) {
|
|
99
|
+
for (const family of families) {
|
|
100
|
+
for (const action of actions) {
|
|
101
|
+
for (let requiredAt = 0; requiredAt < hintEnvelopes.length; requiredAt++) {
|
|
102
|
+
const hints = Object.assign({}, ...hintEnvelopes.map((make, i) => make({ required: i === requiredAt })));
|
|
103
|
+
const p = candidate(build, descriptorFor(action, family), hints);
|
|
104
|
+
const result = evaluate(p);
|
|
105
|
+
check(result.kind === 'block' && result.exitCode === 2, `required hint ${requiredAt}/${String(action)}/${String(family)}/${envelopeName}`, { kind: result.kind, code: result.exitCode });
|
|
106
|
+
check(result.record?.routerActionGate?.required === true, `required hint audit ${requiredAt}/${String(action)}/${String(family)}/${envelopeName}`, { hint: result.record?.routerActionGate });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Every additional field remains gated, including null values, across action/family/envelope variants.
|
|
112
|
+
for (const [envelopeName, build] of envelopes) {
|
|
113
|
+
for (const family of families) {
|
|
114
|
+
for (const action of actions) {
|
|
115
|
+
for (const unsafe of unsafeFields) {
|
|
116
|
+
const p = candidate(build, descriptorFor(action, family));
|
|
117
|
+
p.tool_input = { ...p.tool_input, ...unsafe };
|
|
118
|
+
const result = evaluate(p);
|
|
119
|
+
check(result.kind === 'block' && result.exitCode === 2, `unsafe field ${Object.keys(unsafe)[0]}/${String(action)}/${String(family)}/${envelopeName}`, { kind: result.kind, code: result.exitCode });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Bare/provider/flattened operation shapes and near misses.
|
|
125
|
+
for (const name of [
|
|
126
|
+
'update_pull_request',
|
|
127
|
+
'mcp__github__update_pull_request',
|
|
128
|
+
'mcp__codex_apps__github__update_pull_request',
|
|
129
|
+
'mcp__codex_apps__github_update_pull_request',
|
|
130
|
+
'mcp__demo__update_pull_request',
|
|
131
|
+
]) {
|
|
132
|
+
const result = evaluate({ hook_event_name: 'PreToolUse', session_id: 'names', tool_use_id: `name-${sequence++}`, tool_name: name, tool_input: baseInput });
|
|
133
|
+
check(result.kind === 'allow' && result.record?.actionDescriptor.action === 'mutate' && result.record?.actionDescriptor.classification === 'gated', `valid exact operation ${name}`, { kind: result.kind, action: result.record?.actionDescriptor.action });
|
|
134
|
+
}
|
|
135
|
+
for (const name of ['mcp__demo__update_pull_requests', 'mcp__demo__foo_update_pull_request', 'mcp__demo__update_pull_request_and_delete', 'mcp__codex_apps__github_update_pull_request_extra']) {
|
|
136
|
+
const result = evaluate({ hook_event_name: 'PreToolUse', session_id: 'names', tool_use_id: `near-${sequence++}`, tool_name: name, tool_input: baseInput });
|
|
137
|
+
check(result.kind === 'block' && result.exitCode === 2, `near-miss operation ${name}`, { kind: result.kind, code: result.exitCode });
|
|
138
|
+
}
|
|
139
|
+
console.log(JSON.stringify({ node: process.version, assertions, cases: sequence, failures: failures.length, firstFailures: failures }, null, 2));
|
|
140
|
+
process.exitCode = failures.length ? 1 : 0;
|
|
141
|
+
} finally {
|
|
142
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
143
|
+
}
|