harness-everything 0.4.1 → 0.4.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.
@@ -9,7 +9,7 @@
9
9
  "name": "harness-everything",
10
10
  "source": "./",
11
11
  "description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
12
- "version": "0.4.1",
12
+ "version": "0.4.3",
13
13
  "author": {
14
14
  "name": "Miya Daniel"
15
15
  },
@@ -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.4.1",
4
+ "version": "0.4.3",
5
5
  "author": {
6
6
  "name": "Miya Daniel"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [0.4.3](https://github.com/dyphn1/Harness-everything/compare/v0.4.2...v0.4.3) (2026-09-16)
6
+
7
+ ### Bug Fixes
8
+
9
+ * **codex:** add PreToolUse attribution adapter ([9c54688](https://github.com/dyphn1/Harness-everything/commit/9c54688efc647bb34311dae57abe8766567daf5b))
10
+ * **codex:** package PreToolUse attribution adapter ([93cb459](https://github.com/dyphn1/Harness-everything/commit/93cb4592f2b3f1ebfbed71882dd0f25579b0b469))
11
+ * **codex:** route PreToolUse through attribution adapter ([0ac9221](https://github.com/dyphn1/Harness-everything/commit/0ac922111912813635e9fbc747f7aaa22666bc94))
12
+
13
+ ## [0.4.2](https://github.com/dyphn1/Harness-everything/compare/v0.4.1...v0.4.2) (2026-09-16)
14
+
5
15
  ## [0.4.1](https://github.com/dyphn1/Harness-everything/compare/v0.4.0...v0.4.1) (2026-09-16)
6
16
 
7
17
  ## [0.4.0](https://github.com/dyphn1/Harness-everything/compare/v0.3.6...v0.4.0) (2026-09-16)
@@ -238,7 +238,7 @@ try {
238
238
 
239
239
  const pluginHooks = JSON.parse(fs.readFileSync(path.join(ROOT, 'plugins', 'harness-everything', 'hooks', 'hooks.json'), 'utf8'));
240
240
  const pluginPre = pluginHooks.hooks.PreToolUse.find(group => group.matcher === 'Bash|apply_patch');
241
- check(pluginPre && pluginPre.hooks.some(hook => /action-gate\.js/.test(hook.command)), 'OpenAI plugin wires action-gate to Bash|apply_patch');
241
+ check(pluginPre && pluginPre.hooks.some(hook => /codex-action-gate-pre\.js/.test(hook.command)), 'OpenAI plugin wires the attribution-aware Codex action-gate adapter to Bash|apply_patch');
242
242
  check(!Object.prototype.hasOwnProperty.call(pluginHooks.hooks, 'PostToolUseFailure'), 'Codex plugin does not declare unsupported PostToolUseFailure lifecycle event');
243
243
  const pluginPost = pluginHooks.hooks.PostToolUse.find(group => group.matcher === 'Bash|apply_patch');
244
244
  check(pluginPost && pluginPost.hooks.some(hook => /codex-action-gate-post\.js/.test(hook.command)), 'Codex plugin infers action-gate outcome from supported PostToolUse');
@@ -8,10 +8,13 @@ const path = require('path');
8
8
 
9
9
  const ROOT = path.resolve(__dirname, '..');
10
10
  const actionGate = require(path.join(ROOT, 'hooks', 'scripts', 'action-gate.js'));
11
+ const preAdapter = require(path.join(ROOT, 'hooks', 'scripts', 'codex-action-gate-pre.js'));
11
12
  const permissionRequest = require(path.join(ROOT, 'hooks', 'scripts', 'codex-permission-request.js'));
12
13
 
13
14
  const pluginRoot = path.join(ROOT, 'plugins', 'harness-everything');
14
15
  const pluginHooksPath = path.join(pluginRoot, 'hooks', 'hooks.json');
16
+ const canonicalPre = path.join(ROOT, 'hooks', 'scripts', 'codex-action-gate-pre.js');
17
+ const packagedPre = path.join(pluginRoot, 'hooks', 'scripts', 'codex-action-gate-pre.js');
15
18
  const canonicalPost = path.join(ROOT, 'hooks', 'scripts', 'codex-action-gate-post.js');
16
19
  const packagedPost = path.join(pluginRoot, 'hooks', 'scripts', 'codex-action-gate-post.js');
17
20
  const canonicalPermission = path.join(ROOT, 'hooks', 'scripts', 'codex-permission-request.js');
@@ -49,9 +52,14 @@ function commandPaths(hooks) {
49
52
 
50
53
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-codex-policy-'));
51
54
  const sessionDir = path.join(tempRoot, 'session');
55
+ const attributionDir = path.join(tempRoot, 'attribution');
52
56
  fs.mkdirSync(sessionDir, { recursive: true });
53
57
 
54
58
  try {
59
+ assert.ok(fs.existsSync(canonicalPre), 'Codex PreToolUse adapter must live in canonical hooks/scripts');
60
+ assert.ok(fs.existsSync(packagedPre), 'Codex PreToolUse adapter must be packaged');
61
+ assert.strictEqual(fs.readFileSync(canonicalPre, 'utf8'), fs.readFileSync(packagedPre, 'utf8'), 'canonical and packaged Codex PreToolUse adapters must be byte-equivalent');
62
+
55
63
  assert.ok(fs.existsSync(canonicalPost), 'Codex PostToolUse adapter must live in canonical hooks/scripts so plugin:sync cannot delete it');
56
64
  assert.ok(fs.existsSync(packagedPost), 'Codex PostToolUse adapter must be packaged');
57
65
  assert.strictEqual(fs.readFileSync(canonicalPost, 'utf8'), fs.readFileSync(packagedPost, 'utf8'), 'canonical and packaged Codex PostToolUse adapters must be byte-equivalent');
@@ -63,6 +71,8 @@ try {
63
71
  const hookConfig = JSON.parse(fs.readFileSync(pluginHooksPath, 'utf8'));
64
72
  assert.ok(Array.isArray(hookConfig.hooks.PermissionRequest), 'Codex plugin must declare PermissionRequest');
65
73
  assert.ok(!Object.prototype.hasOwnProperty.call(hookConfig.hooks, 'PostToolUseFailure'), 'Codex plugin must not invent PostToolUseFailure');
74
+ const preCommands = hookConfig.hooks.PreToolUse.flatMap(group => (group.hooks || []).map(hook => hook.command || ''));
75
+ assert.ok(preCommands.some(command => command.includes('codex-action-gate-pre.js')), 'Codex PreToolUse must invoke the attribution-aware action-gate adapter');
66
76
  const permissionCommands = hookConfig.hooks.PermissionRequest.flatMap(group => (group.hooks || []).map(hook => hook.command || ''));
67
77
  assert.ok(permissionCommands.some(command => command.includes('codex-permission-request.js')), 'PermissionRequest must invoke the native approval observer');
68
78
 
@@ -78,6 +88,59 @@ try {
78
88
  assert.strictEqual(pre.stdout, null, 'Codex PreToolUse must never emit permissionDecision=ask');
79
89
  assert.ok(pre.stderr.includes('git-force-push'), 'Codex block reason must identify the matched rule');
80
90
 
91
+ const attributedSafe = preAdapter.processPayload(payload('git status --short', {
92
+ toolUseId: 'codex-attribution-safe',
93
+ turnId: 'turn_attribution_user',
94
+ }), {
95
+ host: 'codex',
96
+ sessionDir,
97
+ ruleTable: table,
98
+ attributionDir,
99
+ runNonce: 'reviewer-user',
100
+ });
101
+ assert.strictEqual(attributedSafe.kind, 'allow', 'attribution must not change an otherwise-safe action-gate result');
102
+ assert.strictEqual(attributedSafe.attribution.enabled, true, 'explicit attribution mode must write hook-entry evidence');
103
+ assert.ok(attributedSafe.attribution.file && fs.existsSync(attributedSafe.attribution.file), 'hook-entry attribution artifact must exist');
104
+ const attribution = JSON.parse(fs.readFileSync(attributedSafe.attribution.file, 'utf8'));
105
+ assert.strictEqual(attribution.eventKind, 'codex-pretooluse-enter');
106
+ assert.strictEqual(attribution.detectedHost, 'codex');
107
+ assert.strictEqual(attribution.toolName, 'Bash');
108
+ assert.strictEqual(attribution.toolUseId, 'codex-attribution-safe');
109
+ assert.strictEqual(attribution.turnId, 'turn_attribution_user');
110
+ assert.strictEqual(attribution.runNonce, 'reviewer-user');
111
+ assert.strictEqual(attribution.payloadHash, actionGate.hashExact('git status --short'));
112
+ assert.ok(!Object.prototype.hasOwnProperty.call(attribution, 'command'), 'attribution evidence must not persist raw command text');
113
+ assert.ok(!Object.prototype.hasOwnProperty.call(attribution, 'toolInput'), 'attribution evidence must not persist raw tool input');
114
+ assert.ok(!Object.prototype.hasOwnProperty.call(attribution, 'cwd'), 'attribution evidence must not persist the working directory');
115
+
116
+ const attributedBlock = preAdapter.processPayload(payload('git push --force origin main', {
117
+ toolUseId: 'codex-attribution-block',
118
+ turnId: 'turn_attribution_auto_review',
119
+ }), {
120
+ host: 'codex',
121
+ sessionDir,
122
+ ruleTable: table,
123
+ attributionDir,
124
+ runNonce: 'reviewer-auto-review',
125
+ });
126
+ assert.strictEqual(attributedBlock.kind, 'block', 'attribution must not weaken the Codex hard-block fallback');
127
+ assert.strictEqual(attributedBlock.exitCode, 2);
128
+ assert.ok(attributedBlock.attribution.file && fs.existsSync(attributedBlock.attribution.file), 'destructive hook entry must be attributable before the block result');
129
+
130
+ const blockedParent = path.join(tempRoot, 'not-a-directory');
131
+ fs.writeFileSync(blockedParent, 'fixture', 'utf8');
132
+ const failedAttribution = preAdapter.processPayload(payload('git push --force origin main', {
133
+ toolUseId: 'codex-attribution-write-failure',
134
+ }), {
135
+ host: 'codex',
136
+ sessionDir,
137
+ ruleTable: table,
138
+ attributionDir: path.join(blockedParent, 'child'),
139
+ });
140
+ assert.strictEqual(failedAttribution.kind, 'block', 'attribution write failure must never authorize a destructive action');
141
+ assert.strictEqual(failedAttribution.exitCode, 2);
142
+ assert.ok(failedAttribution.attribution.error, 'diagnostic write failure should be observable to deterministic tests');
143
+
81
144
  const approvalPayload = payload('git push --force origin main', {
82
145
  event: 'PermissionRequest',
83
146
  toolUseId: undefined,
@@ -111,7 +174,7 @@ try {
111
174
  assert.strictEqual(error.stdout, null, 'PermissionRequest audit failure must not emit allow/deny/ask');
112
175
  assert.ok(/Native Codex approval flow remains authoritative/.test(error.stderr));
113
176
 
114
- console.log('PASS: Codex action-gate uses supported hard blocking plus non-authorizing native PermissionRequest observation.');
177
+ console.log('PASS: Codex action-gate uses supported hard blocking, attributable PreToolUse entry evidence, and non-authorizing PermissionRequest observation.');
115
178
  } finally {
116
179
  fs.rmSync(tempRoot, { recursive: true, force: true });
117
180
  }
@@ -71,6 +71,7 @@ try {
71
71
  assert.ok(fs.existsSync(path.join(pluginRoot, relative)), `sync must preserve every hook target: ${relative}`);
72
72
  }
73
73
 
74
+ assert.ok(fs.existsSync(path.join(pluginRoot, 'hooks', 'scripts', 'codex-action-gate-pre.js')), 'sync must preserve the Codex PreToolUse adapter');
74
75
  assert.ok(fs.existsSync(path.join(pluginRoot, 'hooks', 'scripts', 'codex-action-gate-post.js')), 'sync must preserve the Codex PostToolUse adapter');
75
76
  assert.ok(fs.existsSync(path.join(pluginRoot, 'hooks', 'scripts', 'codex-permission-request.js')), 'sync must preserve the Codex PermissionRequest adapter');
76
77
 
@@ -1,32 +1,38 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const helper = require('./test-helper');
4
+ const { isStableSemVer } = require('../scripts/sync-release-version');
4
5
 
5
6
  console.log('\n[2u] Issue #44 release metadata regression checks...');
6
7
 
7
- const packageJson = JSON.parse(fs.readFileSync(path.join(helper.root, 'package.json'), 'utf8'));
8
8
  const skillText = name => fs.readFileSync(path.join(helper.root, name, 'SKILL.md'), 'utf8');
9
9
  const versionOf = text => {
10
10
  const match = text.match(/^ version:\s*(\S+)$/m);
11
11
  return match && match[1];
12
12
  };
13
13
 
14
+ // Skill metadata records the release in which that skill last changed; it is
15
+ // intentionally not required to equal the current package version. The
16
+ // changed-skill bump policy itself is exercised dynamically by
17
+ // mechanism-2x-release-automation.test.js.
18
+ const fableVersion = versionOf(skillText('fable-mode'));
14
19
  helper.check(
15
- '2u. modified fable-mode skill uses the released package version',
16
- versionOf(skillText('fable-mode')) === packageJson.version,
17
- `fable-mode=${versionOf(skillText('fable-mode'))}, package=${packageJson.version}`
20
+ '2u. fable-mode carries stable release metadata',
21
+ isStableSemVer(fableVersion),
22
+ `fable-mode=${fableVersion}`
18
23
  );
19
24
  for (const nested of ['execution-guardrails', 'fable-haiku', 'fable-opus', 'fable-sonnet']) {
20
25
  helper.check(
21
26
  `2u. fable-mode/${nested} inherits the parent version`,
22
- versionOf(skillText(path.join('fable-mode', nested))) === versionOf(skillText('fable-mode')),
23
- `${nested}=${versionOf(skillText(path.join('fable-mode', nested)))}, parent=${versionOf(skillText('fable-mode'))}`
27
+ versionOf(skillText(path.join('fable-mode', nested))) === fableVersion,
28
+ `${nested}=${versionOf(skillText(path.join('fable-mode', nested)))}, parent=${fableVersion}`
24
29
  );
25
30
  }
31
+ const todoVersion = versionOf(skillText('todo-driven-workflow'));
26
32
  helper.check(
27
- '2u. modified todo-driven-workflow skill uses the released package version',
28
- versionOf(skillText('todo-driven-workflow')) === packageJson.version,
29
- `todo-driven-workflow=${versionOf(skillText('todo-driven-workflow'))}, package=${packageJson.version}`
33
+ '2u. todo-driven-workflow carries stable release metadata',
34
+ isStableSemVer(todoVersion),
35
+ `todo-driven-workflow=${todoVersion}`
30
36
  );
31
37
 
32
38
  const changelog = fs.readFileSync(path.join(helper.root, 'CHANGELOG.md'), 'utf8');
@@ -2,16 +2,26 @@
2
2
  'use strict';
3
3
 
4
4
  const assert = require('assert');
5
+ const fs = require('fs');
6
+ const path = require('path');
5
7
  const { collectContract, renderContractMarkdown, validateContract } = require('../scripts/repository-contract');
6
8
 
7
9
  const contract = collectContract();
8
10
  const failures = validateContract(contract);
9
11
  assert.deepStrictEqual(failures, [], `Repository contract drift:\n- ${failures.join('\n- ')}`);
10
12
 
13
+ const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
14
+ const packageLock = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package-lock.json'), 'utf8'));
15
+ assert.strictEqual(
16
+ packageLock.packages?.['']?.engines?.node,
17
+ packageJson.engines?.node,
18
+ 'package-lock.json root engines.node must mirror package.json engines.node; run npm install --package-lock-only after changing the Node runtime contract',
19
+ );
20
+
11
21
  const rendered = renderContractMarkdown(contract);
12
22
  assert.match(rendered, /Minimum supported Node\.js/);
13
23
  assert.match(rendered, /GitHub Actions runtime majors/);
14
24
  assert.ok(contract.ci.qualityGates.includes('test:consistency'), 'CI contract must expose test:consistency');
15
25
  assert.ok(contract.behavioralEvals.scheduled, 'Behavioral eval schedule must remain visible to the contract');
16
26
 
17
- console.log('Repository contract verified: runtime, workflows, action majors, schedule, gates, and generated docs are synchronized.');
27
+ console.log('Repository contract verified: runtime, lockfile, workflows, action majors, schedule, gates, and generated docs are synchronized.');
@@ -4,23 +4,52 @@ This document records the Codex-specific action-gate contract. It intentionally
4
4
 
5
5
  ## Current official capability boundary
6
6
 
7
- Harness treats the current Codex hooks documentation as the release-behavior contract.
7
+ Harness treats the current Codex hooks documentation as the release-behavior contract. Non-managed command hooks must be reviewed/trusted before they run, so plugin installed/enabled state alone is not live-hook evidence.
8
8
 
9
9
  ### PreToolUse
10
10
 
11
11
  For Codex, Harness may safely block a covered local tool call with a supported deny/block result or exit code 2. Harness must not emit `permissionDecision: "ask"`: Codex currently parses that value but does not support it as an authorization decision, so relying on it would be fail-open.
12
12
 
13
+ The documented Codex `PreToolUse` payload includes `session_id`, `turn_id`, `tool_name`, `tool_use_id`, `tool_input`, and the current permission mode. Harness uses the stable identifiers only for audit/attribution and never treats them as authorization.
14
+
13
15
  The packaged action gate therefore keeps the existing fail-closed behavior for a destructive rule match:
14
16
 
15
17
  ```text
16
18
  covered local tool
17
- -> PreToolUse
19
+ -> codex-action-gate-pre.js
20
+ -> PreToolUse attribution (only when explicitly enabled)
21
+ -> action-gate.js classifier
18
22
  -> Harness destructive rule match
19
23
  -> exit 2 / block
20
24
  ```
21
25
 
22
26
  An internal failure in this pre-action path also remains fail-closed.
23
27
 
28
+ ### Live PreToolUse attribution probe
29
+
30
+ `codex-action-gate-pre.js` is a Codex-specific adapter around the shared action-gate core. Its attribution mode is disabled by default and does not change authorization semantics.
31
+
32
+ Enable it only for live verification by setting:
33
+
34
+ ```text
35
+ HARNESS_ACTION_GATE_ATTRIBUTION_DIR=<known output directory>
36
+ HARNESS_ACTION_GATE_RUN_NONCE=<optional test-run label>
37
+ ```
38
+
39
+ When the `PreToolUse` entry point actually runs, the adapter writes a small JSON artifact containing:
40
+
41
+ - event kind (`codex-pretooluse-enter`);
42
+ - timestamp;
43
+ - detected host;
44
+ - session / turn / tool-use identifiers when available;
45
+ - tool name and permission mode;
46
+ - SHA-256 of the exact tool payload;
47
+ - optional run nonce.
48
+
49
+ The artifact does **not** persist the raw command/tool input, working directory, or transcript path. If diagnostic writing fails, the adapter still delegates to the normal action-gate result; attribution failure cannot convert a destructive block into authorization.
50
+
51
+ For the #110 live matrix, compare at least `approvals_reviewer = "user"` and `approvals_reviewer = "auto_review"` with distinct run nonces. A persisted entry artifact proves that the Codex PreToolUse adapter was launched for that invocation; the resulting action-gate exit/output still determines the Harness disposition.
52
+
24
53
  ### PermissionRequest
25
54
 
26
55
  Codex exposes a separate `PermissionRequest` lifecycle event when Codex is already going to request approval. A PermissionRequest hook can refine that request, but the event is not a mechanism for manufacturing an approval prompt for an otherwise permitted command.
@@ -41,9 +70,15 @@ Codex uses the documented `PostToolUse` lifecycle for completion evidence, inclu
41
70
 
42
71
  `codex-action-gate-post.js` is kept in canonical `hooks/scripts/` and copied into the plugin package by `sync-openai-plugin.js`. This prevents a package sync from deleting the Codex-only adapter.
43
72
 
73
+ ## Approval reviewer boundary
74
+
75
+ `approvals_reviewer = "auto_review"` changes who reviews **eligible approval requests**. The official configuration reference explicitly says it does not change sandboxing or actions already allowed inside the sandbox. The reviewer only sees actions that already need approval.
76
+
77
+ Therefore a command that executes under `auto_review` is not, by itself, evidence that the Harness action-gate was bypassed. First preserve the PreToolUse attribution artifact and determine whether the Harness hook actually fired for that invocation.
78
+
44
79
  ## Native execpolicy prompt rules
45
80
 
46
- Codex rules/execpolicy can express native `prompt` decisions for command prefixes. That is a promising native confirmation path, but Harness does not automatically install or bundle those rules yet.
81
+ Codex rules/execpolicy can express native `prompt` decisions for command prefixes. The current Rules documentation describes this mechanism as controlling commands Codex may run **outside the sandbox**, and the feature remains experimental. That is a promising native confirmation path, but Harness does not automatically install or bundle those rules yet.
47
82
 
48
83
  Reasons:
49
84
 
@@ -68,6 +103,7 @@ A future change may delegate selected destructive rules to Codex-native promptin
68
103
  The evidence must distinguish:
69
104
 
70
105
  ```text
106
+ Harness PreToolUse entry attribution
71
107
  Harness PreToolUse block
72
108
  Codex execpolicy/native prompt
73
109
  PermissionRequest defer/allow/deny
@@ -78,7 +114,7 @@ Package and mechanism tests must not be promoted to `Live verified` in the compa
78
114
 
79
115
  ## Tool coverage
80
116
 
81
- Current Harness PreToolUse enforcement remains wired to the tool set explicitly covered by the package (`Bash|apply_patch`). Codex hooks can cover additional local function/MCP tools, but those tools must not be claimed as action-gated until Harness has a safe classifier/rule representation for their structured inputs. Hosted or specialized tool paths may have different hook behavior and remain outside this guarantee unless separately verified.
117
+ Current Harness PreToolUse enforcement remains wired to the tool set explicitly covered by the package (`Bash|apply_patch`). Codex hooks can cover additional local function/MCP tools, but those tools must not be claimed as action-gated until Harness has a safe classifier/rule representation for their structured inputs. Hosted tools do not use the local function-tool hook path, and specialized tool paths may opt out; Codex documents tool hooks as a useful guardrail rather than a complete enforcement boundary.
82
118
 
83
119
  ## References
84
120
 
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const actionGate = require('./action-gate');
7
+
8
+ const ATTRIBUTION_DIR_ENV = 'HARNESS_ACTION_GATE_ATTRIBUTION_DIR';
9
+ const ATTRIBUTION_NONCE_ENV = 'HARNESS_ACTION_GATE_RUN_NONCE';
10
+ const EVENT_KIND = 'codex-pretooluse-enter';
11
+
12
+ function textOrNull(value, maxLength = 200) {
13
+ if (value === undefined || value === null || value === '') return null;
14
+ return String(value).slice(0, maxLength);
15
+ }
16
+
17
+ function attributionDirectory(options = {}) {
18
+ return options.attributionDir || process.env[ATTRIBUTION_DIR_ENV] || null;
19
+ }
20
+
21
+ function attributionRecord(payload, options = {}) {
22
+ let detectedHost = 'unknown';
23
+ try { detectedHost = actionGate.detectHost(payload, options.host); } catch (_) { /* diagnostic only */ }
24
+
25
+ return {
26
+ schemaVersion: 1,
27
+ eventKind: EVENT_KIND,
28
+ observedAt: new Date().toISOString(),
29
+ hookEventName: actionGate.hookEventOf(payload),
30
+ detectedHost,
31
+ sessionId: textOrNull(payload && (payload.session_id || payload.sessionId)),
32
+ turnId: textOrNull(payload && (payload.turn_id || payload.turnId)),
33
+ toolUseId: textOrNull(payload && (payload.tool_use_id || payload.toolUseId)),
34
+ toolName: textOrNull(payload && (payload.tool_name || payload.toolName)),
35
+ permissionMode: textOrNull(payload && (payload.permission_mode || payload.permissionMode)),
36
+ payloadHash: actionGate.hashExact(actionGate.exactPayloadText(payload)),
37
+ runNonce: textOrNull(options.runNonce || process.env[ATTRIBUTION_NONCE_ENV]),
38
+ };
39
+ }
40
+
41
+ function safeFileToken(value) {
42
+ return String(value || 'event').replace(/[^A-Za-z0-9._-]+/g, '_').slice(0, 120) || 'event';
43
+ }
44
+
45
+ function writeAttribution(payload, options = {}) {
46
+ const dir = attributionDirectory(options);
47
+ if (!dir) return { enabled: false, file: null, record: null, error: null };
48
+
49
+ const record = attributionRecord(payload, options);
50
+ const key = record.toolUseId || record.turnId || record.payloadHash.slice(0, 20);
51
+ const file = path.join(dir, `${Date.now()}-${process.pid}-${safeFileToken(key)}.json`);
52
+ const temp = `${file}.${process.pid}.tmp`;
53
+
54
+ try {
55
+ fs.mkdirSync(dir, { recursive: true });
56
+ fs.writeFileSync(temp, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
57
+ fs.renameSync(temp, file);
58
+ return { enabled: true, file, record, error: null };
59
+ } catch (err) {
60
+ try { if (fs.existsSync(temp)) fs.rmSync(temp, { force: true }); } catch (_) { /* best effort */ }
61
+ return {
62
+ enabled: true,
63
+ file: null,
64
+ record,
65
+ error: err && err.message ? err.message : String(err),
66
+ };
67
+ }
68
+ }
69
+
70
+ function processPayload(payload, options = {}) {
71
+ // Attribution is deliberately diagnostic-only. A write failure cannot turn a
72
+ // Harness decision into allow, deny, or ask; the action-gate remains the sole
73
+ // authorization path for this hook invocation.
74
+ const attribution = writeAttribution(payload, options);
75
+ let result;
76
+ try {
77
+ result = actionGate.processEvent(payload, options);
78
+ } catch (err) {
79
+ result = actionGate.internalErrorDecision(payload, err, options);
80
+ }
81
+ return { ...result, attribution };
82
+ }
83
+
84
+ function emitResult(result) {
85
+ if (result.stderr) console.error(result.stderr);
86
+ if (result.stdout) process.stdout.write(`${JSON.stringify(result.stdout)}\n`);
87
+ return result.exitCode || 0;
88
+ }
89
+
90
+ function runPayload(payload) {
91
+ return emitResult(processPayload(payload));
92
+ }
93
+
94
+ function setExitCodeIfDefined(code) {
95
+ if (Number.isInteger(code)) process.exitCode = code;
96
+ }
97
+
98
+ function readStdinAndRun() {
99
+ if (process.stdin.isTTY) return runPayload(null);
100
+ let raw = '';
101
+ let finished = false;
102
+ const finish = () => {
103
+ if (finished) return undefined;
104
+ finished = true;
105
+ let payload = {};
106
+ try { payload = JSON.parse(raw || '{}'); } catch (err) {
107
+ return emitResult(actionGate.internalErrorDecision({}, new Error(`invalid hook JSON: ${err.message}`)));
108
+ }
109
+ return runPayload(payload);
110
+ };
111
+
112
+ const timeoutMsRaw = Number(process.env.HARNESS_ACTION_GATE_STDIN_TIMEOUT_MS);
113
+ const timeoutMs = Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0 ? timeoutMsRaw : 500;
114
+ const timer = setTimeout(() => setExitCodeIfDefined(finish()), timeoutMs);
115
+ process.stdin.setEncoding('utf8');
116
+ process.stdin.on('data', chunk => { raw += chunk; });
117
+ process.stdin.on('end', () => {
118
+ clearTimeout(timer);
119
+ setExitCodeIfDefined(finish());
120
+ });
121
+ process.stdin.on('error', err => {
122
+ clearTimeout(timer);
123
+ setExitCodeIfDefined(emitResult(actionGate.internalErrorDecision({}, err)));
124
+ });
125
+ return 0;
126
+ }
127
+
128
+ if (require.main === module) {
129
+ setExitCodeIfDefined(readStdinAndRun());
130
+ }
131
+
132
+ module.exports = {
133
+ ATTRIBUTION_DIR_ENV,
134
+ ATTRIBUTION_NONCE_ENV,
135
+ EVENT_KIND,
136
+ attributionDirectory,
137
+ attributionRecord,
138
+ processPayload,
139
+ readStdinAndRun,
140
+ writeAttribution,
141
+ };
@@ -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.4.1",
4
+ "version": "0.4.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.4.1",
3
+ "version": "0.4.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.4.1",
3
+ "version": "0.4.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",
@@ -36,8 +36,8 @@
36
36
  "hooks": [
37
37
  {
38
38
  "type": "command",
39
- "command": "node \"$PLUGIN_ROOT/hooks/scripts/action-gate.js\"",
40
- "commandWindows": "node \"%PLUGIN_ROOT%\\hooks\\scripts\\action-gate.js\"",
39
+ "command": "node \"$PLUGIN_ROOT/hooks/scripts/codex-action-gate-pre.js\"",
40
+ "commandWindows": "node \"%PLUGIN_ROOT%\\hooks\\scripts\\codex-action-gate-pre.js\"",
41
41
  "timeout": 15,
42
42
  "statusMessage": "Checking Harness pre-action approval gate"
43
43
  },
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const actionGate = require('./action-gate');
7
+
8
+ const ATTRIBUTION_DIR_ENV = 'HARNESS_ACTION_GATE_ATTRIBUTION_DIR';
9
+ const ATTRIBUTION_NONCE_ENV = 'HARNESS_ACTION_GATE_RUN_NONCE';
10
+ const EVENT_KIND = 'codex-pretooluse-enter';
11
+
12
+ function textOrNull(value, maxLength = 200) {
13
+ if (value === undefined || value === null || value === '') return null;
14
+ return String(value).slice(0, maxLength);
15
+ }
16
+
17
+ function attributionDirectory(options = {}) {
18
+ return options.attributionDir || process.env[ATTRIBUTION_DIR_ENV] || null;
19
+ }
20
+
21
+ function attributionRecord(payload, options = {}) {
22
+ let detectedHost = 'unknown';
23
+ try { detectedHost = actionGate.detectHost(payload, options.host); } catch (_) { /* diagnostic only */ }
24
+
25
+ return {
26
+ schemaVersion: 1,
27
+ eventKind: EVENT_KIND,
28
+ observedAt: new Date().toISOString(),
29
+ hookEventName: actionGate.hookEventOf(payload),
30
+ detectedHost,
31
+ sessionId: textOrNull(payload && (payload.session_id || payload.sessionId)),
32
+ turnId: textOrNull(payload && (payload.turn_id || payload.turnId)),
33
+ toolUseId: textOrNull(payload && (payload.tool_use_id || payload.toolUseId)),
34
+ toolName: textOrNull(payload && (payload.tool_name || payload.toolName)),
35
+ permissionMode: textOrNull(payload && (payload.permission_mode || payload.permissionMode)),
36
+ payloadHash: actionGate.hashExact(actionGate.exactPayloadText(payload)),
37
+ runNonce: textOrNull(options.runNonce || process.env[ATTRIBUTION_NONCE_ENV]),
38
+ };
39
+ }
40
+
41
+ function safeFileToken(value) {
42
+ return String(value || 'event').replace(/[^A-Za-z0-9._-]+/g, '_').slice(0, 120) || 'event';
43
+ }
44
+
45
+ function writeAttribution(payload, options = {}) {
46
+ const dir = attributionDirectory(options);
47
+ if (!dir) return { enabled: false, file: null, record: null, error: null };
48
+
49
+ const record = attributionRecord(payload, options);
50
+ const key = record.toolUseId || record.turnId || record.payloadHash.slice(0, 20);
51
+ const file = path.join(dir, `${Date.now()}-${process.pid}-${safeFileToken(key)}.json`);
52
+ const temp = `${file}.${process.pid}.tmp`;
53
+
54
+ try {
55
+ fs.mkdirSync(dir, { recursive: true });
56
+ fs.writeFileSync(temp, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
57
+ fs.renameSync(temp, file);
58
+ return { enabled: true, file, record, error: null };
59
+ } catch (err) {
60
+ try { if (fs.existsSync(temp)) fs.rmSync(temp, { force: true }); } catch (_) { /* best effort */ }
61
+ return {
62
+ enabled: true,
63
+ file: null,
64
+ record,
65
+ error: err && err.message ? err.message : String(err),
66
+ };
67
+ }
68
+ }
69
+
70
+ function processPayload(payload, options = {}) {
71
+ // Attribution is deliberately diagnostic-only. A write failure cannot turn a
72
+ // Harness decision into allow, deny, or ask; the action-gate remains the sole
73
+ // authorization path for this hook invocation.
74
+ const attribution = writeAttribution(payload, options);
75
+ let result;
76
+ try {
77
+ result = actionGate.processEvent(payload, options);
78
+ } catch (err) {
79
+ result = actionGate.internalErrorDecision(payload, err, options);
80
+ }
81
+ return { ...result, attribution };
82
+ }
83
+
84
+ function emitResult(result) {
85
+ if (result.stderr) console.error(result.stderr);
86
+ if (result.stdout) process.stdout.write(`${JSON.stringify(result.stdout)}\n`);
87
+ return result.exitCode || 0;
88
+ }
89
+
90
+ function runPayload(payload) {
91
+ return emitResult(processPayload(payload));
92
+ }
93
+
94
+ function setExitCodeIfDefined(code) {
95
+ if (Number.isInteger(code)) process.exitCode = code;
96
+ }
97
+
98
+ function readStdinAndRun() {
99
+ if (process.stdin.isTTY) return runPayload(null);
100
+ let raw = '';
101
+ let finished = false;
102
+ const finish = () => {
103
+ if (finished) return undefined;
104
+ finished = true;
105
+ let payload = {};
106
+ try { payload = JSON.parse(raw || '{}'); } catch (err) {
107
+ return emitResult(actionGate.internalErrorDecision({}, new Error(`invalid hook JSON: ${err.message}`)));
108
+ }
109
+ return runPayload(payload);
110
+ };
111
+
112
+ const timeoutMsRaw = Number(process.env.HARNESS_ACTION_GATE_STDIN_TIMEOUT_MS);
113
+ const timeoutMs = Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0 ? timeoutMsRaw : 500;
114
+ const timer = setTimeout(() => setExitCodeIfDefined(finish()), timeoutMs);
115
+ process.stdin.setEncoding('utf8');
116
+ process.stdin.on('data', chunk => { raw += chunk; });
117
+ process.stdin.on('end', () => {
118
+ clearTimeout(timer);
119
+ setExitCodeIfDefined(finish());
120
+ });
121
+ process.stdin.on('error', err => {
122
+ clearTimeout(timer);
123
+ setExitCodeIfDefined(emitResult(actionGate.internalErrorDecision({}, err)));
124
+ });
125
+ return 0;
126
+ }
127
+
128
+ if (require.main === module) {
129
+ setExitCodeIfDefined(readStdinAndRun());
130
+ }
131
+
132
+ module.exports = {
133
+ ATTRIBUTION_DIR_ENV,
134
+ ATTRIBUTION_NONCE_ENV,
135
+ EVENT_KIND,
136
+ attributionDirectory,
137
+ attributionRecord,
138
+ processPayload,
139
+ readStdinAndRun,
140
+ writeAttribution,
141
+ };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "harness-everything",
4
- "version": "0.4.1",
4
+ "version": "0.4.3",
5
5
  "description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
6
6
  "author": {
7
7
  "name": "Miya Daniel",