harness-everything 0.41.1 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/ci.yml +2 -2
- package/CHANGELOG.md +12 -0
- package/ci/action-gate.test.js +24 -0
- package/ci/mechanism-codex-prompt-pass-through.test.js +94 -0
- package/ci/mechanism-task-git-authorization.test.js +145 -0
- package/docs/codex-action-gate.md +29 -0
- package/docs/doc-workflow-reconciliation.md +13 -13
- package/docs/document-lifecycle.md +7 -5
- package/docs/platform-capabilities.md +21 -0
- package/hooks/scripts/action-gate.js +25 -4
- package/hooks/scripts/codex-user-prompt.js +8 -0
- 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 +25 -4
- package/plugins/harness-everything/hooks/scripts/codex-user-prompt.js +8 -0
- 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.41.
|
|
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.
|
|
4
|
+
"version": "0.41.3",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Miya Daniel"
|
|
7
7
|
},
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -36,9 +36,9 @@ jobs:
|
|
|
36
36
|
- uses: actions/checkout@v7
|
|
37
37
|
- uses: actions/setup-node@v7
|
|
38
38
|
with:
|
|
39
|
-
node-version: '24.
|
|
39
|
+
node-version: '24.22.1'
|
|
40
40
|
- name: Verify exact patched runtime
|
|
41
|
-
run: node -e "if (process.versions.node !== '24.
|
|
41
|
+
run: node -e "if (process.versions.node !== '24.22.1') process.exit(1)"
|
|
42
42
|
- name: Exercise real evaluator subprocess and repeated resident teardown
|
|
43
43
|
run: node --test ci/mechanism-system-one-resident.test.js
|
|
44
44
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
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
|
+
|
|
11
|
+
## [0.41.2](https://github.com/dyphn1/Harness-everything/compare/v0.41.1...v0.41.2) (2026-10-25)
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **codex:** bind explicit action approvals ([eeec25c](https://github.com/dyphn1/Harness-everything/commit/eeec25c204bae89fa293e77a1d7de9e8f7135e13))
|
|
16
|
+
|
|
5
17
|
## [0.41.1](https://github.com/dyphn1/Harness-everything/compare/v0.41.0...v0.41.1) (2026-10-23)
|
|
6
18
|
|
|
7
19
|
### Bug Fixes
|
package/ci/action-gate.test.js
CHANGED
|
@@ -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
|
+
}
|
|
@@ -124,3 +124,32 @@ Current Harness PreToolUse enforcement remains wired to the tool set explicitly
|
|
|
124
124
|
- Codex configuration reference: https://developers.openai.com/codex/config-reference
|
|
125
125
|
|
|
126
126
|
Related tracking: #82, #85, #98, #100, #109, #110.
|
|
127
|
+
|
|
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.
|
|
@@ -5,13 +5,13 @@ status: active
|
|
|
5
5
|
|
|
6
6
|
# Document workflow reconciliation — Phase 3b S1 runtime
|
|
7
7
|
|
|
8
|
-
**Status:
|
|
9
|
-
|
|
10
|
-
This document refines the [document lifecycle contract](document-lifecycle.md#phase-3b--narrow-integration-contract-s1-
|
|
8
|
+
**Status: the runtime ships default OFF; opt in with `HARNESS_DOC_RECONCILIATION_PILOT=1`.
|
|
9
|
+
With the pilot ON, F4 ([#358](https://github.com/dyphn1/Harness-everything/pull/358)) grants limited positive credit for exactly `doc-lint` and `link-check`, from a host-captured run of the installed doc-check on Claude Code 2.1.309 (live runs 4–5). S3 provider integration is pending; E1 has not started.**
|
|
10
|
+
This document refines the [document lifecycle contract](document-lifecycle.md#phase-3b--narrow-integration-contract-s1-default-off-f4-limited-credit).
|
|
11
11
|
The [accepted E0](https://github.com/dyphn1/Harness-everything/issues/341#issuecomment-6308604128)
|
|
12
12
|
permits this limited, reversible consumer under the
|
|
13
13
|
[owner's E0/E1 decision](https://github.com/dyphn1/Harness-everything/issues/325#issuecomment-6283237637).
|
|
14
|
-
Current evidence establishes deterministic mechanism/package behavior
|
|
14
|
+
Current evidence establishes deterministic mechanism/package behavior and bounded Claude Code 2.1.309 live credit runs; it is not E1 GO.
|
|
15
15
|
|
|
16
16
|
## Entry gates and ownership
|
|
17
17
|
|
|
@@ -336,12 +336,12 @@ Preparation assertion rows do not carry `reportOrigin`; the S1 runtime report ca
|
|
|
336
336
|
marker is never presented as a production report.
|
|
337
337
|
## S1 — switch, trusted scope and fail-closed no-credit gate
|
|
338
338
|
|
|
339
|
-
**S1 runtime ships default OFF (`HARNESS_DOC_RECONCILIATION_PILOT=1` opts in).
|
|
339
|
+
**S1 runtime ships default OFF (`HARNESS_DOC_RECONCILIATION_PILOT=1` opts in). With the pilot ON, F4 (#358) grants limited positive credit for `doc-lint` and `link-check` only; S3 provider integration is pending and E1 has not started.**
|
|
340
340
|
The [accepted E0](https://github.com/dyphn1/Harness-everything/issues/341#issuecomment-6308604128)
|
|
341
341
|
permits this limited implementation; the [posted execution plan](https://github.com/dyphn1/Harness-everything/issues/325)
|
|
342
|
-
orders Docs → RED → GREEN → independent review → green CI for each slice. S1
|
|
343
|
-
negative-credit slice. S2
|
|
344
|
-
positive reconciliation and freshness
|
|
342
|
+
orders Docs → RED → GREEN → independent review → green CI for each slice. S1 was the temporary
|
|
343
|
+
negative-credit slice; F4 added the limited positive credit. The S2 requirements (trusted producer records, independent review provenance,
|
|
344
|
+
positive reconciliation and freshness) gate E1 observation. S3 adds optional
|
|
345
345
|
provider suggestions. None of these slices authorizes wider #320 integration.
|
|
346
346
|
|
|
347
347
|
### Switch and scope decision
|
|
@@ -1080,8 +1080,8 @@ provider, discovery execution, public checker change or new attestation service/
|
|
|
1080
1080
|
## Implementation and acceptance after E0
|
|
1081
1081
|
|
|
1082
1082
|
S1 ships the default-OFF switch, narrow disposition/completion no-credit integration and runtime
|
|
1083
|
-
status reporting.
|
|
1084
|
-
|
|
1083
|
+
status reporting. F4 adds limited positive reconciliation behind the same switch. S3's optional
|
|
1084
|
+
provider adapter remains pending. Require every expected applicable MUST and
|
|
1085
1085
|
freshness check before positive credit, plus source/package compatibility, negative cases and
|
|
1086
1086
|
relevant green CI including Windows. The S1 suites establish their tested mechanism/package
|
|
1087
1087
|
contracts; they do not establish independent host provenance, live-host loading or architectural
|
|
@@ -1090,7 +1090,7 @@ remain separate gates.
|
|
|
1090
1090
|
|
|
1091
1091
|
## F4: credited reconciliation (Claude-only, pilot ON)
|
|
1092
1092
|
|
|
1093
|
-
Status: **implemented behind the pilot switch; mechanism and package evidence, plus
|
|
1093
|
+
Status: **implemented behind the pilot switch; mechanism and package evidence, plus live credit runs on Claude Code 2.1.309 (runs 4–5), which credited exactly `doc-lint` and `link-check`.** Mechanism tests cover credit for `doc-lint` and `link-check` (a registered `doc-check` run) and for an independent `quality-review` (a host reviewer with bound path digests). The earlier Claude Code 2.1.308 capture (macOS, one session) proves field availability only. Deterministic mechanism tests prove the chain. Design source: the A3 advisor report (`.worktrees/packets/feat-path/A3-advisor-report.md`, a local coordination file, not a shipped doc).
|
|
1094
1094
|
|
|
1095
1095
|
### Whole path
|
|
1096
1096
|
|
|
@@ -1200,8 +1200,8 @@ claude -p "<fixed capture prompt, containing the exact check command>" \
|
|
|
1200
1200
|
### Scope and limits
|
|
1201
1201
|
|
|
1202
1202
|
- The first positive scope is a body-only edit to an existing governed reference, with no derived declaration, code, test, PRD or ADR normative change. Its applicable MUSTs are lint and links.
|
|
1203
|
-
- Simulated replay and
|
|
1204
|
-
- Not claimed: cryptographic attestation, a sandbox, a forced review provider, default-ON, live-host behaviour beyond the
|
|
1203
|
+
- Simulated replay and the recorded live credit runs are not E1 GO, and they do not permit a wider rollout.
|
|
1204
|
+
- Not claimed: cryptographic attestation, a sandbox, a forced review provider, default-ON, live-host behaviour beyond the recorded runs, or support for OpenCode or Codex.
|
|
1205
1205
|
|
|
1206
1206
|
**Executor rationale.** The owner's text (#325, 2026-10-21) says the reviewer "must differ from the author's `session_id` and from every executor principal". An executor principal is the principal of execution evidence used in this assessment. A reviewer that read or computed digests with plain tool calls is not an executor. A reviewer whose registered doc-check run is refused still counts, which keeps the rule fail-closed.
|
|
1207
1207
|
|
|
@@ -15,19 +15,21 @@ This table distinguishes merged mechanism code from end-to-end workflow integrat
|
|
|
15
15
|
| 2 — roles, IDs, aliases, legacy, supersession, index, D6.9 PRD revisions | **Merged** in #327; do not rebuild this lifecycle logic for Phase 3 |
|
|
16
16
|
| Step-type prerequisite (#326) | **Complete:** PR #328 merged; Harness-owned `doc-change` is explicitly declared, has no implicit TDD binding, and is not auto-inserted by router/System One |
|
|
17
17
|
| 3a — document facts, obligations and evidence producer | **Merged** in #334: `harness-everything/scripts/doc-obligations.js` and `doc-evidence.js`. H3/H4 have *partial deterministic mechanism* coverage, not integrated workflow evidence. Known limitations: caller-attested envelope; `configDigest` presence-only; `candidateDigest` restricted to `docs/prd/`; missing independent task-scope binding. Deleted and old rename paths are bound to trusted per-path baseline evidence ([doc-evidence-baseline-binding.md](doc-evidence-baseline-binding.md), #335 fixed by #342), with workflow-disposition reconciliation left out of scope |
|
|
18
|
-
| 3b — pilot-local provider suggestions and workflow evidence reconciliation | **S1 runtime ships default OFF** (opt in with `HARNESS_DOC_RECONCILIATION_PILOT=1`), with trusted scope assessment and fail-closed withholding
|
|
18
|
+
| 3b — pilot-local provider suggestions and workflow evidence reconciliation | **S1 runtime ships default OFF** (opt in with `HARNESS_DOC_RECONCILIATION_PILOT=1`), with trusted scope assessment and fail-closed withholding. **F4 (#358) adds limited positive credit** for exactly `doc-lint` and `link-check` from a host-captured run of the installed doc-check (Claude Code only; live runs on 2.1.309). See [S1 switch, trusted scope and no-credit contract](doc-workflow-reconciliation.md#s1--switch-trusted-scope-and-fail-closed-no-credit-gate). S3 pure advisory provider suggestions shipped in 0.37.0 (not wired); E1 has not started. Accepted #341 E0 permits this limited reversible consumer; the live runs are not E1 GO; preserve existing required-binding compatibility |
|
|
19
19
|
| 3c — H1–H4 and paired-task evaluation | **Not started.** Mechanism tests alone are insufficient for architecture promotion; compare frozen baseline and treatment on real bounded tasks, success/failure first, then conditional cost and latency |
|
|
20
|
-
| 1b — shared documentation craft and family metadata | **
|
|
21
|
-
| 4 — derived-view generators and `describes:` drift | **
|
|
20
|
+
| 1b — shared documentation craft and family metadata | **Shared craft delivered** in F2 (#356): shared references with thin skill consumers. Family metadata is not established. A small optional logical-family metadata seam may be piloted; physical skill directory moves remain blocked on #320 identity/path separation |
|
|
21
|
+
| 4 — derived-view generators and `describes:` drift | **Regenerate-equals delivered** in F3 (#357) for authorized generators; `describes:` drift reminders and broader maintenance are not established. Preserve generator authorization, deterministic regenerate-equals and scoped reminder semantics |
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
<a id="phase-3b--narrow-integration-contract-s1-shipped-default-off-positive-reconciliation-not-shipped"></a>
|
|
24
|
+
|
|
25
|
+
### Phase 3b — narrow integration contract (S1 default OFF; F4 limited credit)
|
|
24
26
|
|
|
25
27
|
The preparation design, test-only provider/trust contracts, and qualified RED replay are complete:
|
|
26
28
|
[Document workflow reconciliation](doc-workflow-reconciliation.md),
|
|
27
29
|
[case corpus](../ci/fixtures/doc-obligations/reconciliation-prep/cases.json),
|
|
28
30
|
[pending-RED registry](../ci/fixtures/doc-obligations/reconciliation-prep/pending-red.json), and
|
|
29
31
|
[preparation suite](../ci/mechanism-325-3b-preparation.test.js). #341 E0 is accepted; S1 ships the
|
|
30
|
-
default-OFF
|
|
32
|
+
default-OFF gate, and F4 (#358) adds limited positive credit for `doc-lint` and `link-check` behind the same switch. S3 provider integration remains pending.
|
|
31
33
|
|
|
32
34
|
- **Inputs:** explicit active `doc-change` step, D6.5 diff facts, Phase 3a obligations/evidence, available provider *metadata*, and host capabilities.
|
|
33
35
|
- **Entry and activation gates (#341):** parallel preparatory work — provider metadata, adapter/interface design and RED fixtures — may proceed while #341 Phase 0A–0C runs, but the **runtime** Evidence Reconciliation must not merge or activate until **#341 E0** is reviewed and accepted (initial evidence read/binding/outcome conformance, mutation and read-order compatibility, source/package parity, zero false MUST PASS on applicable adversarial cases, and applicable green CI including Windows). After E0, only the **bounded, opt-in, reversible** documentation consumer is eligible for runtime integration at the existing verification/disposition path (no second router, forced provider, new lock, or weakened authorization, with a documented rollback), and it supplies the observed exposure for **#341 E1** (initial target 2–3 genuine development/review cycles with sufficient case mix, not elapsed time). **#341 E1 GO / REVISE / NO-GO gates expansion beyond the limited pilot, not the pilot's own observation** (missing or unrepresentative exposure is `UNCHECKED`, never an automatic GO). The H1–H4 paired-task architectural gate below stays required for any #320 promotion in addition to E1. Shared evidence-read/binding/decision defects route to #341 (fixture-first correction, regression replay, re-observation); documentation policy and the trusted host/VCS producer remain #325 deliverables here.
|
|
@@ -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,6 +135,18 @@ 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
|
|
|
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.
|
|
149
|
+
|
|
129
150
|
## Install/uninstall ownership boundary
|
|
130
151
|
|
|
131
152
|
The general installer merges shared files only through Harness-owned markers and removes only manifest-tracked artifacts. Malformed configuration and pre-existing user-owned files fail closed. Install/uninstall round-trip coverage protects user skills and runs on Linux, Windows, and macOS fixtures.
|
|
@@ -9,7 +9,6 @@ const {
|
|
|
9
9
|
getWorkspaceRoot,
|
|
10
10
|
getSessionDir,
|
|
11
11
|
} = require('./lib/harness-state');
|
|
12
|
-
|
|
13
12
|
const RULES_PATH = process.env.HARNESS_ACTION_GATE_RULES_PATH || path.join(__dirname, 'action-gate-rules.json');
|
|
14
13
|
const ASK_CAPABLE_HOSTS = new Set(['claude']);
|
|
15
14
|
const VALID_SCOPES = new Set(['always', 'outside-scratch']);
|
|
@@ -52,10 +51,12 @@ function toolInputOf(payload) {
|
|
|
52
51
|
function stableValue(value) {
|
|
53
52
|
if (Array.isArray(value)) return value.map(stableValue);
|
|
54
53
|
if (!value || typeof value !== 'object') return value;
|
|
54
|
+
// Use a null-prototype accumulator so an own `__proto__` key is stored as data
|
|
55
|
+
// instead of mutating the accumulator's prototype (which would drop it).
|
|
55
56
|
return Object.keys(value).sort().reduce((acc, key) => {
|
|
56
57
|
acc[key] = stableValue(value[key]);
|
|
57
58
|
return acc;
|
|
58
|
-
},
|
|
59
|
+
}, Object.create(null));
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
function exactPayloadText(payload) {
|
|
@@ -132,10 +133,25 @@ function normalizeActionDescriptor(payload) {
|
|
|
132
133
|
};
|
|
133
134
|
}
|
|
134
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
|
+
|
|
135
143
|
function structuredRule(payload) {
|
|
136
144
|
const descriptor = normalizeActionDescriptor(payload);
|
|
137
145
|
if (descriptor.toolFamily === 'shell') return { descriptor, rule: null };
|
|
138
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
|
+
}
|
|
139
155
|
if (descriptor.classification === 'gated') {
|
|
140
156
|
return {
|
|
141
157
|
descriptor,
|
|
@@ -145,7 +161,6 @@ function structuredRule(payload) {
|
|
|
145
161
|
},
|
|
146
162
|
};
|
|
147
163
|
}
|
|
148
|
-
const hint = routerActionGateHint(payload);
|
|
149
164
|
if (hint && hint.required) {
|
|
150
165
|
return {
|
|
151
166
|
descriptor,
|
|
@@ -445,11 +460,17 @@ function recordDecision(payload, rule, table, host, disposition, options = {}, e
|
|
|
445
460
|
return record;
|
|
446
461
|
}
|
|
447
462
|
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
|
|
448
467
|
function evaluatePreToolUse(payload, options = {}) {
|
|
449
468
|
const context = sessionContext(payload, options.sessionDir);
|
|
450
469
|
const table = options.ruleTable || loadRuleTable(options.rulePath || RULES_PATH);
|
|
451
470
|
const structured = structuredRule(payload);
|
|
452
471
|
const rule = structured.rule || classify(payload, table.rules, context.sessionDir);
|
|
472
|
+
const host = detectHost(payload, options.host);
|
|
473
|
+
|
|
453
474
|
if (!rule) {
|
|
454
475
|
return {
|
|
455
476
|
kind: 'allow',
|
|
@@ -462,7 +483,6 @@ function evaluatePreToolUse(payload, options = {}) {
|
|
|
462
483
|
};
|
|
463
484
|
}
|
|
464
485
|
|
|
465
|
-
const host = detectHost(payload, options.host);
|
|
466
486
|
const reason = decisionReason(rule, table);
|
|
467
487
|
if (hostSupportsAsk(host) && gatePolicy(options) === ALWAYS_ASK_POLICY) {
|
|
468
488
|
const record = recordDecision(payload, rule, table, host, 'pending-approval', { ...options, sessionDir: context.sessionDir });
|
|
@@ -655,4 +675,5 @@ module.exports = {
|
|
|
655
675
|
validateRuleTable,
|
|
656
676
|
normalizeActionDescriptor,
|
|
657
677
|
structuredRule,
|
|
678
|
+
isRoutinePullRequestCreation,
|
|
658
679
|
};
|
|
@@ -22,6 +22,14 @@ function finish() {
|
|
|
22
22
|
if (finished) return;
|
|
23
23
|
finished = true;
|
|
24
24
|
clearTimeout(timer);
|
|
25
|
+
|
|
26
|
+
let payload = null;
|
|
27
|
+
try { payload = JSON.parse(input || '{}'); } catch (_) { payload = null; }
|
|
28
|
+
|
|
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.
|
|
25
33
|
if (!ROUTER) process.exit(0);
|
|
26
34
|
const result = spawnSync(process.execPath, [ROUTER], { input, encoding: 'utf8', env: process.env, timeout: 25000 });
|
|
27
35
|
const stderr = String(result.stderr || '');
|
package/package.json
CHANGED
|
@@ -9,7 +9,6 @@ const {
|
|
|
9
9
|
getWorkspaceRoot,
|
|
10
10
|
getSessionDir,
|
|
11
11
|
} = require('./lib/harness-state');
|
|
12
|
-
|
|
13
12
|
const RULES_PATH = process.env.HARNESS_ACTION_GATE_RULES_PATH || path.join(__dirname, 'action-gate-rules.json');
|
|
14
13
|
const ASK_CAPABLE_HOSTS = new Set(['claude']);
|
|
15
14
|
const VALID_SCOPES = new Set(['always', 'outside-scratch']);
|
|
@@ -52,10 +51,12 @@ function toolInputOf(payload) {
|
|
|
52
51
|
function stableValue(value) {
|
|
53
52
|
if (Array.isArray(value)) return value.map(stableValue);
|
|
54
53
|
if (!value || typeof value !== 'object') return value;
|
|
54
|
+
// Use a null-prototype accumulator so an own `__proto__` key is stored as data
|
|
55
|
+
// instead of mutating the accumulator's prototype (which would drop it).
|
|
55
56
|
return Object.keys(value).sort().reduce((acc, key) => {
|
|
56
57
|
acc[key] = stableValue(value[key]);
|
|
57
58
|
return acc;
|
|
58
|
-
},
|
|
59
|
+
}, Object.create(null));
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
function exactPayloadText(payload) {
|
|
@@ -132,10 +133,25 @@ function normalizeActionDescriptor(payload) {
|
|
|
132
133
|
};
|
|
133
134
|
}
|
|
134
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
|
+
|
|
135
143
|
function structuredRule(payload) {
|
|
136
144
|
const descriptor = normalizeActionDescriptor(payload);
|
|
137
145
|
if (descriptor.toolFamily === 'shell') return { descriptor, rule: null };
|
|
138
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
|
+
}
|
|
139
155
|
if (descriptor.classification === 'gated') {
|
|
140
156
|
return {
|
|
141
157
|
descriptor,
|
|
@@ -145,7 +161,6 @@ function structuredRule(payload) {
|
|
|
145
161
|
},
|
|
146
162
|
};
|
|
147
163
|
}
|
|
148
|
-
const hint = routerActionGateHint(payload);
|
|
149
164
|
if (hint && hint.required) {
|
|
150
165
|
return {
|
|
151
166
|
descriptor,
|
|
@@ -445,11 +460,17 @@ function recordDecision(payload, rule, table, host, disposition, options = {}, e
|
|
|
445
460
|
return record;
|
|
446
461
|
}
|
|
447
462
|
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
|
|
448
467
|
function evaluatePreToolUse(payload, options = {}) {
|
|
449
468
|
const context = sessionContext(payload, options.sessionDir);
|
|
450
469
|
const table = options.ruleTable || loadRuleTable(options.rulePath || RULES_PATH);
|
|
451
470
|
const structured = structuredRule(payload);
|
|
452
471
|
const rule = structured.rule || classify(payload, table.rules, context.sessionDir);
|
|
472
|
+
const host = detectHost(payload, options.host);
|
|
473
|
+
|
|
453
474
|
if (!rule) {
|
|
454
475
|
return {
|
|
455
476
|
kind: 'allow',
|
|
@@ -462,7 +483,6 @@ function evaluatePreToolUse(payload, options = {}) {
|
|
|
462
483
|
};
|
|
463
484
|
}
|
|
464
485
|
|
|
465
|
-
const host = detectHost(payload, options.host);
|
|
466
486
|
const reason = decisionReason(rule, table);
|
|
467
487
|
if (hostSupportsAsk(host) && gatePolicy(options) === ALWAYS_ASK_POLICY) {
|
|
468
488
|
const record = recordDecision(payload, rule, table, host, 'pending-approval', { ...options, sessionDir: context.sessionDir });
|
|
@@ -655,4 +675,5 @@ module.exports = {
|
|
|
655
675
|
validateRuleTable,
|
|
656
676
|
normalizeActionDescriptor,
|
|
657
677
|
structuredRule,
|
|
678
|
+
isRoutinePullRequestCreation,
|
|
658
679
|
};
|
|
@@ -22,6 +22,14 @@ function finish() {
|
|
|
22
22
|
if (finished) return;
|
|
23
23
|
finished = true;
|
|
24
24
|
clearTimeout(timer);
|
|
25
|
+
|
|
26
|
+
let payload = null;
|
|
27
|
+
try { payload = JSON.parse(input || '{}'); } catch (_) { payload = null; }
|
|
28
|
+
|
|
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.
|
|
25
33
|
if (!ROUTER) process.exit(0);
|
|
26
34
|
const result = spawnSync(process.execPath, [ROUTER], { input, encoding: 'utf8', env: process.env, timeout: 25000 });
|
|
27
35
|
const stderr = String(result.stderr || '');
|
|
@@ -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.41.
|
|
4
|
+
"version": "0.41.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",
|
package/sbom.cdx.json
CHANGED
|
@@ -2,23 +2,23 @@
|
|
|
2
2
|
"$schema": "https://cyclonedx.org/schema/bom-1.7.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.7",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:e4948d5f-cae4-5b4e-84bc-19997b97c420",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-10-
|
|
8
|
+
"timestamp": "2026-10-25T06:53:03.342Z",
|
|
9
9
|
"tools": [
|
|
10
10
|
{
|
|
11
11
|
"vendor": "Harness Everything",
|
|
12
12
|
"name": "generate-sbom.js",
|
|
13
|
-
"version": "0.41.
|
|
13
|
+
"version": "0.41.3"
|
|
14
14
|
}
|
|
15
15
|
],
|
|
16
16
|
"component": {
|
|
17
17
|
"type": "application",
|
|
18
|
-
"bom-ref": "pkg:npm/harness-everything@0.41.
|
|
18
|
+
"bom-ref": "pkg:npm/harness-everything@0.41.3",
|
|
19
19
|
"name": "harness-everything",
|
|
20
|
-
"version": "0.41.
|
|
21
|
-
"purl": "pkg:npm/harness-everything@0.41.
|
|
20
|
+
"version": "0.41.3",
|
|
21
|
+
"purl": "pkg:npm/harness-everything@0.41.3"
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"components": [
|
|
@@ -2033,7 +2033,7 @@
|
|
|
2033
2033
|
]
|
|
2034
2034
|
},
|
|
2035
2035
|
{
|
|
2036
|
-
"ref": "pkg:npm/harness-everything@0.41.
|
|
2036
|
+
"ref": "pkg:npm/harness-everything@0.41.3",
|
|
2037
2037
|
"dependsOn": [
|
|
2038
2038
|
"pkg:npm/%40commitlint/cli@19.8.1",
|
|
2039
2039
|
"pkg:npm/%40commitlint/config-conventional@19.8.1",
|