harness-everything 0.41.2 → 0.41.3

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.
@@ -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.41.2",
13
+ "version": "0.41.3",
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.41.2",
4
+ "version": "0.41.3",
5
5
  "author": {
6
6
  "name": "Miya Daniel"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [0.41.3](https://github.com/dyphn1/Harness-everything/compare/v0.41.2...v0.41.3) (2026-10-25)
6
+
7
+ ### Bug Fixes
8
+
9
+ * **codex:** drop format-bound task approvals ([7125220](https://github.com/dyphn1/Harness-everything/commit/71252205e469cda13744a733ff2dca6bbff5db47))
10
+
5
11
  ## [0.41.2](https://github.com/dyphn1/Harness-everything/compare/v0.41.1...v0.41.2) (2026-10-25)
6
12
 
7
13
  ### Bug Fixes
@@ -273,6 +273,30 @@ try {
273
273
  check(empty.degraded === false && empty.rules.length === 0, 'valid empty rule table does not silently activate built-in defaults');
274
274
  check(actionGate.evaluatePreToolUse(payload('git push --force origin main', { toolUseId: 'empty' }), { host: 'claude', sessionDir, ruleTable: empty }).kind === 'allow', 'empty-table mutation control disables detection, proving production non-empty assertion is meaningful');
275
275
 
276
+ // Independent full-input fingerprint regression: an own JSON __proto__ key must
277
+ // remain part of the structured payload digest (null-prototype stableValue fix).
278
+ const protoInput = JSON.parse('{"__proto__":{"changed":true},"record_id":"record-123"}');
279
+ const protoPayload = {
280
+ session_id: 'phase5-test',
281
+ hook_event_name: 'PreToolUse',
282
+ tool_name: 'mcp__demo__delete_record',
283
+ tool_use_id: 'proto-fingerprint',
284
+ tool_input: protoInput,
285
+ cwd: ROOT,
286
+ };
287
+ const basePayload = {
288
+ session_id: 'phase5-test',
289
+ hook_event_name: 'PreToolUse',
290
+ tool_name: 'mcp__demo__delete_record',
291
+ tool_use_id: 'proto-fingerprint',
292
+ tool_input: { record_id: 'record-123' },
293
+ cwd: ROOT,
294
+ };
295
+ const protoText = actionGate.exactPayloadText(protoPayload);
296
+ const baseText = actionGate.exactPayloadText(basePayload);
297
+ check(protoText.includes('__proto__'), 'null-prototype serialization preserves an own __proto__ key');
298
+ check(actionGate.hashExact(protoText) !== actionGate.hashExact(baseText), 'own __proto__ key changes the full-input digest');
299
+
276
300
  const execPayload = payload('npm publish', { toolUseId: 'exec-ok' });
277
301
  actionGate.evaluatePreToolUse(execPayload, { host: 'claude', sessionDir, ruleTable: table });
278
302
  const execResult = actionGate.processEvent({ ...execPayload, hook_event_name: 'PostToolUse' }, { sessionDir });
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // RED contract: Codex UserPromptSubmit must pass every prompt, including the
5
+ // former exact `HARNESS APPROVE <id>` spelling, to the normal router. The hook
6
+ // is transport, not a natural-language authorization parser.
7
+ const assert = require('assert');
8
+ const fs = require('fs');
9
+ const os = require('os');
10
+ const path = require('path');
11
+ const { spawnSync } = require('child_process');
12
+
13
+ const ROOT = path.resolve(__dirname, '..');
14
+ const SOURCE_HOOK = path.join(ROOT, 'hooks', 'scripts', 'codex-user-prompt.js');
15
+
16
+ let failed = 0;
17
+ function check(condition, message) {
18
+ if (condition) console.log(` PASS ${message}`);
19
+ else { console.error(` FAIL ${message}`); failed++; }
20
+ }
21
+
22
+ // Dependency control: source hook must exist. Child Node is run with the
23
+ // executable that is already running this test.
24
+ check(fs.existsSync(SOURCE_HOOK), 'source codex-user-prompt.js exists');
25
+
26
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-codex-prompt-passthrough-'));
27
+ const fixtureRoot = path.join(tempRoot, 'fixture');
28
+ const hookDir = path.join(fixtureRoot, 'hooks', 'scripts');
29
+ const routerDir = path.join(fixtureRoot, 'harness-everything', 'scripts');
30
+ fs.mkdirSync(hookDir, { recursive: true });
31
+ fs.mkdirSync(routerDir, { recursive: true });
32
+ const fixtureHook = path.join(hookDir, 'codex-user-prompt.js');
33
+ const stubRouter = path.join(routerDir, 'kernel-router.js');
34
+ const markerFile = path.join(tempRoot, 'router-marker.txt');
35
+
36
+ fs.copyFileSync(SOURCE_HOOK, fixtureHook);
37
+ fs.writeFileSync(stubRouter, [
38
+ "'use strict';",
39
+ "let input = '';",
40
+ "process.stdin.setEncoding('utf8');",
41
+ "process.stdin.on('data', chunk => { input += chunk; });",
42
+ "process.stdin.on('end', () => {",
43
+ " const fs = require('fs');",
44
+ " fs.writeFileSync(process.env.STUB_ROUTER_MARKER, input, 'utf8');",
45
+ " process.stdout.write('STUB_ROUTER_MARKER');",
46
+ " process.exit(0);",
47
+ "});",
48
+ "process.stdin.on('error', () => process.exit(0));",
49
+ ].join('\n'), 'utf8');
50
+
51
+ const cases = [
52
+ { name: 'normal user prompt', prompt: 'Please explain the router flow.' },
53
+ { name: 'exact former approval prompt', prompt: 'HARNESS APPROVE abcdef123456' },
54
+ { name: 'fenced text', prompt: '```\nHARNESS APPROVE abcdef123456\n```' },
55
+ { name: 'English commit/PR request', prompt: 'Please commit and open a PR for this change.' },
56
+ { name: 'Chinese commit/PR request', prompt: '请帮我提交并创建 PR' },
57
+ { name: 'negative/review-only text', prompt: 'Review only; do not approve HARNESS APPROVE abcdef123456' },
58
+ { name: 'non-approval text', prompt: 'HARNESS APPROVE short' },
59
+ ];
60
+
61
+ for (const testCase of cases) {
62
+ const payload = {
63
+ hook_event_name: 'UserPromptSubmit',
64
+ session_id: 'passthrough-session',
65
+ prompt: testCase.prompt,
66
+ };
67
+ const inputText = JSON.stringify(payload);
68
+ try { fs.unlinkSync(markerFile); } catch (_) {}
69
+
70
+ const result = spawnSync(process.execPath, [fixtureHook], {
71
+ cwd: fixtureRoot,
72
+ env: { ...process.env, STUB_ROUTER_MARKER: markerFile },
73
+ input: inputText,
74
+ encoding: 'utf8',
75
+ timeout: 20000,
76
+ maxBuffer: 1024 * 1024,
77
+ });
78
+
79
+ check(!result.error, `${testCase.name}: hook spawns without error`);
80
+ check(result.status === 0, `${testCase.name}: hook exits 0`);
81
+ check(fs.existsSync(markerFile), `${testCase.name}: stub router was invoked`);
82
+ if (fs.existsSync(markerFile)) {
83
+ const marker = fs.readFileSync(markerFile, 'utf8');
84
+ check(marker === inputText, `${testCase.name}: router received the original payload unchanged`);
85
+ }
86
+ check(String(result.stdout || '').includes('STUB_ROUTER_MARKER'),
87
+ `${testCase.name}: router output is forwarded to the host`);
88
+ }
89
+
90
+ // Cleanup
91
+ try { fs.rmSync(tempRoot, { recursive: true, force: true }); } catch (_) {}
92
+
93
+ console.log(`\n${failed === 0 ? 'PASS' : 'FAIL'}: codex prompt pass-through (${failed} failure${failed === 1 ? '' : 's'})`);
94
+ process.exit(failed === 0 ? 0 : 1);
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const assert = require('assert');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+ const { spawnSync } = require('child_process');
9
+
10
+ const ROOT = path.resolve(__dirname, '..');
11
+ const actionGate = require(path.join(ROOT, 'hooks', 'scripts', 'action-gate.js'));
12
+ const PRE_HOOK = path.join(ROOT, 'hooks', 'scripts', 'codex-action-gate-pre.js');
13
+
14
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-task-git-auth-'));
15
+ const stateHome = path.join(tempRoot, 'state');
16
+ fs.mkdirSync(stateHome, { recursive: true });
17
+
18
+ const env = {
19
+ ...process.env,
20
+ HARNESS_ACTION_GATE_HOST: 'codex',
21
+ HARNESS_STATE_HOME: stateHome,
22
+ HARNESS_ACTION_GATE_STDIN_TIMEOUT_MS: '2000',
23
+ };
24
+
25
+ function run(payload) {
26
+ const result = spawnSync(process.execPath, [PRE_HOOK], {
27
+ cwd: tempRoot,
28
+ env,
29
+ input: JSON.stringify(payload),
30
+ encoding: 'utf8',
31
+ timeout: 20000,
32
+ maxBuffer: 1024 * 1024,
33
+ });
34
+ assert.ifError(result.error);
35
+ return result;
36
+ }
37
+
38
+ function payload(toolName, toolInput, extra = {}) {
39
+ return {
40
+ hook_event_name: 'PreToolUse',
41
+ session_id: 'task-git-auth-codex-session',
42
+ turn_id: 'turn-task-git-auth',
43
+ tool_use_id: `call-${Math.random().toString(16).slice(2)}`,
44
+ tool_name: toolName,
45
+ tool_input: toolInput,
46
+ permission_mode: 'default',
47
+ cwd: tempRoot,
48
+ ...extra,
49
+ };
50
+ }
51
+
52
+ const actualPrPayload = payload('mcp__codex_apps__github__create_pull_request', {
53
+ repository_full_name: 'openai/codex',
54
+ base: 'main',
55
+ head: 'feature/task-authorization',
56
+ title: 'Phase complete PR',
57
+ body: '## Summary\n\nPhase complete PR created after task authorization.',
58
+ draft: true,
59
+ });
60
+
61
+ try {
62
+ assert.ok(fs.existsSync(PRE_HOOK), 'Codex PreToolUse entrypoint must exist');
63
+
64
+ // Read control on the real Codex path.
65
+ const readResult = run(payload('mcp__demo__get_pull_request', { pull_request: 1 }));
66
+ assert.strictEqual(readResult.status, 0, 'read-only structured action must pass on Codex');
67
+ assert.strictEqual(String(readResult.stdout || '').trim(), '', 'read control must not emit a Harness permission decision');
68
+
69
+ // Dangerous controls remain hard Codex blocks.
70
+ for (const [toolName, toolInput] of [
71
+ ['mcp__demo__merge_pull_request', { id: 'target' }],
72
+ ['mcp__demo__delete_pull_request', { id: 'target' }],
73
+ ['mcp__demo__deploy_release', { id: 'target' }],
74
+ ['mcp__demo__publish_package', { id: 'target' }],
75
+ ['mcp__demo__send_message', { id: 'target' }],
76
+ ]) {
77
+ const result = run(payload(toolName, toolInput));
78
+ assert.strictEqual(result.status, 2, `${toolName} must remain a Codex block`);
79
+ }
80
+ const forcePush = run(payload('Bash', { command: 'git push --force origin main' }));
81
+ assert.strictEqual(forcePush.status, 2, 'git push --force must remain a Codex block');
82
+
83
+ // Normal non-destructive commit keeps its existing ungated behavior.
84
+ const normalCommit = run(payload('Bash', { command: 'git commit -m "phase complete"' }));
85
+ assert.strictEqual(normalCommit.status, 0, 'normal git commit must remain ungated on Codex');
86
+
87
+ // Misleading names containing create_pull_request must not inherit the exemption.
88
+ for (const toolName of [
89
+ 'mcp__demo__create_pull_request_and_delete',
90
+ 'mcp__demo__delete_create_pull_request',
91
+ 'mcp__demo__create_pull_requests',
92
+ 'mcp__demo__foo_create_pull_request',
93
+ ]) {
94
+ const result = run(payload(toolName, { id: 'target' }));
95
+ assert.strictEqual(result.status, 2, `${toolName} must remain a Codex block`);
96
+ }
97
+
98
+ // Explicit required gate is carried in the supported payload hint shape
99
+ // (payload.workflowPlan.actionGate — the shape routerActionGateHint reads),
100
+ // not a made-up options API.
101
+ const requiredDecision = actionGate.evaluatePreToolUse({
102
+ ...actualPrPayload,
103
+ tool_use_id: 'call-required-gate-witness',
104
+ workflowPlan: { actionGate: { required: true, reasonCodes: ['external-side-effect'], disposition: 'pending-approval' } },
105
+ }, {
106
+ host: 'codex',
107
+ ruleTable: actionGate.loadRuleTable(path.join(ROOT, 'hooks', 'scripts', 'action-gate-rules.json')),
108
+ });
109
+ assert.strictEqual(requiredDecision.kind, 'block', 'required-gate PR must remain gated');
110
+ assert.strictEqual(requiredDecision.exitCode, 2, 'required-gate PR must remain a Codex block');
111
+ assert.ok(requiredDecision.rule, 'required-gate PR must match a gated rule');
112
+ assert.strictEqual(requiredDecision.rule.id, 'structured-mutate', 'required-gate PR must keep structured-mutate');
113
+
114
+ // Recognized PR creation is a routine host-deferred operation. Its descriptor
115
+ // stays a mutation; it is not read or safe, and it must not use Harness consent.
116
+ const descriptor = actionGate.normalizeActionDescriptor(actualPrPayload);
117
+ assert.strictEqual(descriptor.action, 'mutate', 'recognized PR creation must remain classified as a mutation');
118
+ assert.notStrictEqual(descriptor.classification, 'safe', 'recognized PR creation must not be safe/read');
119
+ const prResult = run(actualPrPayload);
120
+ assert.strictEqual(prResult.status, 0, 'recognized PR creation must be host-deferred on Codex (currently exits 2: RED)');
121
+ assert.strictEqual(String(prResult.stdout || '').trim(), '', 'recognized PR creation must not emit permissionDecision allow/ask');
122
+ assert.ok(!/HARNESS APPROVE/i.test(`${prResult.stdout || ''}\n${prResult.stderr || ''}`), 'recognized PR creation must not require a Harness approval phrase');
123
+
124
+ // Provider-neutral exact create_pull_request operation, if used, has the same contract.
125
+ const neutralResult = run(payload('mcp__demo__create_pull_request', {
126
+ repository: 'acme/harness',
127
+ head: 'feature/task-authorization',
128
+ base: 'main',
129
+ title: 'Phase complete PR',
130
+ body: 'Provider-neutral PR body.',
131
+ }));
132
+ assert.strictEqual(neutralResult.status, 0, 'exact create_pull_request operation must be host-deferred on Codex (currently exits 2: RED)');
133
+
134
+ // Genuine mutations remain blocked, but the fixed-format approval protocol is retired.
135
+ const legacyMutation = run(payload('mcp__demo__update_issue', { issue_id: 1, title: 'changed' }));
136
+ assert.strictEqual(legacyMutation.status, 2, 'genuine mutation must remain blocked on Codex');
137
+ assert.ok(!/HARNESS APPROVE/i.test(`${legacyMutation.stdout || ''}\n${legacyMutation.stderr || ''}`), 'genuine mutation must not expose the retired Harness approval phrase');
138
+
139
+ console.log('PASS: task git authorization Codex controls and host-deferred PR creation');
140
+ } catch (err) {
141
+ console.error(err && err.stack ? err.stack : err);
142
+ process.exitCode = 1;
143
+ } finally {
144
+ try { fs.rmSync(tempRoot, { recursive: true, force: true }); } catch (_) {}
145
+ }
@@ -125,24 +125,31 @@ Current Harness PreToolUse enforcement remains wired to the tool set explicitly
125
125
 
126
126
  Related tracking: #82, #85, #98, #100, #109, #110.
127
127
 
128
- ## Codex explicit authorization (scoped consent)
129
-
130
- In addition to the hard block above, a blocked Codex structured mutation exposes a
131
- pending challenge scoped to the host session, tool name, and canonical full
132
- tool-input digest. The pending challenge has no wall-clock TTL. The challenge id
133
- appears in the block output as `challenge id: <id>`. Only scoped structured mutations (`rule.id` `structured-mutate`)
134
- issue or consume grants; other actions keep ordinary blocking.
135
-
136
- The only approval source is a real `UserPromptSubmit` hook receiving the exact
137
- entire prompt `HARNESS APPROVE <challengeId>` with no additional text. That
138
- event binds the host session and its consent turn to a one-use grant for the
139
- same session, consent turn, tool, and full-input digest.
140
-
141
- A matching retry passes once and atomically consumes the grant. Missing,
142
- mismatched, cross-session, cross-turn, and replayed grants remain blocked. The
143
- one-use grant is bound to the consent turn and fails after a turn change. Tool
144
- input containing an `authorization` JSON claim never grants.
145
-
146
- On a matching retry the guard does not block and emits no `permissionDecision=allow`;
147
- Codex native permission controls remain authoritative. This is cooperative local
148
- hook state, not tamperproof, and carries no live-host proof.
128
+ ## Prompt handling and authorization
129
+
130
+ Codex `UserPromptSubmit` prompts pass through to the normal router unchanged.
131
+ Harness does not parse, bind, or intercept any fixed approval syntax in user
132
+ prompts. Native agent task-scope authorization and native permissions remain
133
+ authoritative.
134
+
135
+ Routine exact `create_pull_request` operations are host-deferred (below). All
136
+ other risky actions (merge/delete/deploy/publish/send/force-push and unrelated
137
+ genuine mutations) remain hard PreToolUse blocks; an explicitly required
138
+ `actionGate` hint still gates. These are deterministic mechanism changes, not
139
+ live-host proof.
140
+
141
+ ## Routine task Git operations (host-deferred)
142
+
143
+ Ordinary exact `create_pull_request` operations (including
144
+ `mcp__codex_apps__github__create_pull_request` and provider-neutral
145
+ `mcp__*__create_pull_request`) are host-deferred: Harness does not issue an
146
+ additional challenge merely because the generic `create` heuristic classifies
147
+ them as `mutate`. Their action descriptor stays `mutate` and non-safe; Harness
148
+ does not pretend a write is a read or emit `permissionDecision` allow/ask.
149
+
150
+ Native agent task-scope authorization and native permissions remain authoritative.
151
+ An explicitly supplied required `actionGate` hint still gates the call. All other
152
+ risky actions (merge/delete/deploy/publish/send/force-push and unrelated genuine
153
+ mutations) remain gated; exact operation matching is used, with no substring
154
+ exemption, so misleading names such as `create_pull_requests` or
155
+ `foo_create_pull_request` still block.
@@ -25,6 +25,15 @@ The repository routing contract uses **step-scoped workflow selection**. Normali
25
25
 
26
26
  This is a **repository/agent contract**, not an automatic upgrade to any platform row below. A hook can prove that the contract was injected and record an agent's binding disposition; neither proves that a live host actually loaded or followed the referenced content. Instruction-only surfaces can carry the same rule without mechanically enforcing it. Promoting behavior to live-host compliance requires retained host/session evidence under the #82 compatibility program. Claude and local OpenAI packages carry workflow entry/completion **reminders** plus separate Rule-of-3/permission boundaries, but this package/mechanism change does not upgrade `platform-compatibility.json` live-host status values. See [workflow runtime](workflow-runtime.md) for scope, worktree isolation, and failure behavior. OpenCode and instruction-only/public Skills-only surfaces do not acquire equivalent runtime gating. Shell inspection is not a filesystem sandbox; absent hooks, agent-writable state, and indirect script effects remain limits.
27
27
 
28
+ ### Action-gate mechanism vs. host semantic boundary
29
+
30
+ Routine exact `create_pull_request` operations are host-deferred at the mechanism level:
31
+ their descriptor remains a mutation (not read/safe), and no Harness consent or
32
+ `permissionDecision` is emitted. Native agent task-scope authorization and native
33
+ permissions stay authoritative; an explicit required `actionGate` hint still gates.
34
+ This is a deterministic mechanism change, not live-host proof, and does not
35
+ upgrade any platform row's live-host status.
36
+
28
37
  ## Current capability summary
29
38
 
30
39
  | Platform surface | Standalone skills | Plugin support | Hooks/lifecycle | Project scope | Global scope | Current evidence boundary |
@@ -126,22 +135,17 @@ The compatibility path registers hook commands in Codex user configuration and c
126
135
 
127
136
  The public OpenAI Skills-only plugin is narrower again. Its ZIP contains the packaged `skills/` tree and referenced files, but not the local `.codex-plugin` lifecycle hooks. Public submission readiness is therefore a package/form contract, not evidence of public-directory approval or live hook execution.
128
137
 
129
- ### Codex explicit UserPromptSubmit consent (mechanism only)
130
-
131
- Codex has a local `UserPromptSubmit` exact-challenge consent path for a blocked
132
- structured mutation. The blocked call exposes a pending challenge scoped to host
133
- session, tool name, and canonical full tool-input digest. The pending challenge has
134
- no wall-clock TTL. Only a real
135
- `UserPromptSubmit` hook receiving the exact entire prompt
136
- `HARNESS APPROVE <challengeId>` with no additional text can bind that challenge to
137
- a host session and consent turn. The resulting one-use grant only lets the
138
- matching retry pass Harness's guard; it does not emit `permissionDecision=allow`
139
- and does not supersede native Codex permissions. Missing, mismatched, cross-session, cross-turn, and replayed grants remain
140
- blocked. The one-use grant is bound to the consent turn and fails after a turn
141
- change. Tool-input
142
- authorization JSON never grants. This is cooperative local hook state: it is not
143
- tamperproof and carries no live-host verification. Codex ask-style decisions are
144
- not supported.
138
+ ### Codex prompt pass-through and authorization (mechanism only)
139
+
140
+ Codex `UserPromptSubmit` prompts pass through to the normal router unchanged;
141
+ Harness does not parse or intercept a fixed approval syntax. Native agent
142
+ task-scope authorization and native permissions remain authoritative. Routine
143
+ exact `create_pull_request` operations are host-deferred at the mechanism level:
144
+ their descriptor remains a mutation (not read/safe), and no Harness consent or
145
+ `permissionDecision` is emitted. All other risky actions remain hard PreToolUse
146
+ blocks; an explicitly required `actionGate` hint still gates. These are
147
+ deterministic mechanism changes, not live-host proof. Codex ask-style decisions
148
+ are not supported.
145
149
 
146
150
  ## Install/uninstall ownership boundary
147
151
 
@@ -9,8 +9,6 @@ const {
9
9
  getWorkspaceRoot,
10
10
  getSessionDir,
11
11
  } = require('./lib/harness-state');
12
- const codexActionAuthorization = require('./lib/codex-action-authorization');
13
-
14
12
  const RULES_PATH = process.env.HARNESS_ACTION_GATE_RULES_PATH || path.join(__dirname, 'action-gate-rules.json');
15
13
  const ASK_CAPABLE_HOSTS = new Set(['claude']);
16
14
  const VALID_SCOPES = new Set(['always', 'outside-scratch']);
@@ -135,10 +133,25 @@ function normalizeActionDescriptor(payload) {
135
133
  };
136
134
  }
137
135
 
136
+ function isRoutinePullRequestCreation(toolName) {
137
+ const name = String(toolName || '');
138
+ const segments = name.split('__');
139
+ const last = segments[segments.length - 1];
140
+ return (last === 'create_pull_request' || name === 'create_pull_request');
141
+ }
142
+
138
143
  function structuredRule(payload) {
139
144
  const descriptor = normalizeActionDescriptor(payload);
140
145
  if (descriptor.toolFamily === 'shell') return { descriptor, rule: null };
141
146
  if (descriptor.classification === 'safe') return { descriptor, rule: null };
147
+ // Routine PR creation is a host-deferred mutation, not a Harness-gated one:
148
+ // the generic create=>mutate heuristic must not add an extra challenge. The
149
+ // descriptor stays a mutation (not read/safe). An explicit required
150
+ // actionGate hint still gates it.
151
+ const hint = routerActionGateHint(payload);
152
+ if (isRoutinePullRequestCreation(descriptor.toolName) && !(hint && hint.required)) {
153
+ return { descriptor, rule: null };
154
+ }
142
155
  if (descriptor.classification === 'gated') {
143
156
  return {
144
157
  descriptor,
@@ -148,7 +161,6 @@ function structuredRule(payload) {
148
161
  },
149
162
  };
150
163
  }
151
- const hint = routerActionGateHint(payload);
152
164
  if (hint && hint.required) {
153
165
  return {
154
166
  descriptor,
@@ -449,19 +461,8 @@ function recordDecision(payload, rule, table, host, disposition, options = {}, e
449
461
  }
450
462
 
451
463
 
452
- function isScopedCodexStructuredMutation(structured, rule) {
453
- return Boolean(
454
- rule && rule.id === 'structured-mutate' &&
455
- structured && structured.descriptor &&
456
- structured.descriptor.toolFamily !== 'shell' &&
457
- structured.descriptor.action === 'mutate' &&
458
- structured.descriptor.classification === 'gated'
459
- );
460
- }
461
464
 
462
- function safeInline(value) {
463
- return String(value == null ? '' : value).replace(/[\r\n\t\u0000-\u001f\u007f]/g, '?').slice(0, 200);
464
- }
465
+
465
466
 
466
467
  function evaluatePreToolUse(payload, options = {}) {
467
468
  const context = sessionContext(payload, options.sessionDir);
@@ -470,23 +471,6 @@ function evaluatePreToolUse(payload, options = {}) {
470
471
  const rule = structured.rule || classify(payload, table.rules, context.sessionDir);
471
472
  const host = detectHost(payload, options.host);
472
473
 
473
- // Codex explicit authorization applies only to scoped structured mutations.
474
- // Shell-governed and other structured actions keep ordinary blocking.
475
- if (host === 'codex' && isScopedCodexStructuredMutation(structured, rule)) {
476
- const consumed = codexActionAuthorization.consumeGrantIfValid(payload, { sessionDir: context.sessionDir });
477
- if (consumed && consumed.authorized) {
478
- return {
479
- kind: 'allow',
480
- exitCode: 0,
481
- stdout: null,
482
- stderr: null,
483
- rule: null,
484
- table,
485
- descriptor: structured.descriptor,
486
- };
487
- }
488
- }
489
-
490
474
  if (!rule) {
491
475
  return {
492
476
  kind: 'allow',
@@ -514,25 +498,11 @@ function evaluatePreToolUse(payload, options = {}) {
514
498
  const record = recordDecision(payload, rule, table, host, 'rejected', { ...options, sessionDir: context.sessionDir }, {
515
499
  rejectionReason: 'host-ask-decision-unverified-or-unsupported',
516
500
  });
517
- let challengeSuffix = '';
518
- let detailsSuffix = '';
519
- if (host === 'codex' && isScopedCodexStructuredMutation(structured, rule)) {
520
- const challengeId = codexActionAuthorization.issueChallenge(payload, {
521
- sessionDir: context.sessionDir,
522
- toolName: toolNameOf(payload),
523
- digest: (structured.descriptor && structured.descriptor.payloadHash) || hashExact(exactPayloadText(payload)),
524
- });
525
- if (challengeId) {
526
- challengeSuffix = ` challenge id: ${challengeId}`;
527
- const digest = (structured.descriptor && structured.descriptor.payloadHash) || hashExact(exactPayloadText(payload));
528
- detailsSuffix = ` Tool: ${safeInline(toolNameOf(payload))}. Input SHA-256: ${digest}. Approve with: HARNESS APPROVE ${challengeId}.`;
529
- }
530
- }
531
501
  return {
532
502
  kind: 'block',
533
503
  exitCode: 2,
534
504
  stdout: null,
535
- stderr: `${reason} Host '${host}' has no Harness-verified ask-style decision path, so the action is blocked.${challengeSuffix}${detailsSuffix}`,
505
+ stderr: `${reason} Host '${host}' has no Harness-verified ask-style decision path, so the action is blocked.`,
536
506
  rule,
537
507
  table,
538
508
  record,
@@ -705,4 +675,5 @@ module.exports = {
705
675
  validateRuleTable,
706
676
  normalizeActionDescriptor,
707
677
  structuredRule,
678
+ isRoutinePullRequestCreation,
708
679
  };
@@ -26,22 +26,10 @@ function finish() {
26
26
  let payload = null;
27
27
  try { payload = JSON.parse(input || '{}'); } catch (_) { payload = null; }
28
28
 
29
- // Codex explicit authorization: only an exact host-submitted consent prompt
30
- // may bind a pending challenge to the host session + consent turn. No
31
- // natural-language inference, no tool-input claim.
32
- if (payload && payload.hook_event_name === 'UserPromptSubmit') {
33
- const prompt = payload.prompt;
34
- const match = String(prompt || '').match(/^HARNESS APPROVE ([A-Za-z0-9._-]{8,})$/);
35
- if (match) {
36
- try {
37
- const auth = require('./lib/codex-action-authorization');
38
- auth.bindConsent(payload, { challengeId: match[1] });
39
- } catch (_) { /* consent state is best-effort; failures do not authorize */ }
40
- process.exit(0);
41
- return;
42
- }
43
- }
44
-
29
+ // All prompts, including any legacy approval spelling, pass through unchanged
30
+ // to the normal router. Harness does not parse or intercept authorization
31
+ // syntax in user prompts; native agent/task scope and native permissions
32
+ // remain authoritative.
45
33
  if (!ROUTER) process.exit(0);
46
34
  const result = spawnSync(process.execPath, [ROUTER], { input, encoding: 'utf8', env: process.env, timeout: 25000 });
47
35
  const stderr = String(result.stderr || '');
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "harness-enforcement",
3
3
  "description": "Repository inventory for the opencode enforcement plugin and its standalone helper",
4
- "version": "0.41.2",
4
+ "version": "0.41.3",
5
5
  "runtime": "index.mjs",
6
6
  "hooks": {
7
7
  "verify": "hooks/verify.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.41.2",
3
+ "version": "0.41.3",
4
4
  "description": "Harness OS: An Orchestrated Agent Operating System for AI-driven Development",
5
5
  "main": "harness-everything/scripts/bootstrap.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.41.2",
3
+ "version": "0.41.3",
4
4
  "description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
5
5
  "author": {
6
6
  "name": "Miya Daniel",