harness-everything 0.42.0 → 0.42.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +12 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/README.md +68 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/green-final.log +162 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/green.log +158 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/live-installed-runtime-rejection.json +31 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-action-family-cross-product.js +143 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-action-family-cross-product.log +7 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-codex-action-gate-prehook-smoke.js +78 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-codex-action-gate-prehook-smoke.log +8 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-review.md +151 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/original-rejection.json +31 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/red.log +158 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-descriptor-green.log +192 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-descriptor-red.log +192 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-family-green.log +206 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-family-red.log +206 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-hints-green.log +174 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-hints-red.log +174 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/verification.md +31 -0
- package/benchmarks/results/doc-evidence-e1/README.md +68 -0
- package/benchmarks/results/doc-evidence-e1/evidence.json +360 -0
- package/benchmarks/results/doc-evidence-e1/pilot-report.json +175 -0
- package/benchmarks/results/h1-provider-independence-qualification/README.md +26 -3
- package/benchmarks/results/live-host/opencode-d1-d2-2026-10-25/README.md +30 -0
- package/benchmarks/results/live-host/opencode-d1-d2-2026-10-25/evidence.json +128 -0
- package/ci/action-gate.test.js +86 -0
- package/ci/mechanism-2aa-codex-action-gate-policy.test.js +47 -0
- package/ci/mechanism-task-git-authorization.test.js +12 -0
- package/docs/codex-action-gate.md +27 -1
- package/docs/doc-workflow-reconciliation.md +4 -3
- package/docs/document-lifecycle.md +3 -3
- package/docs/platform-capabilities.md +8 -2
- package/hooks/scripts/action-gate.js +35 -8
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/action-gate.js +35 -8
- package/plugins/harness-everything/plugin.json +1 -1
- package/sbom.cdx.json +7 -7
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"clientDate": "2026-10-25",
|
|
4
|
+
"classification": "retained-negative-observations-only",
|
|
5
|
+
"publicReproduction": "LOCAL_ONLY supporting packets, fixtures, and runner are not shipped; this PR does not reproduce full runs",
|
|
6
|
+
"productBaseline": "37c97046cb1e620c4a3585b4da5a2593501a48be",
|
|
7
|
+
"runs": [
|
|
8
|
+
{
|
|
9
|
+
"id": "D1",
|
|
10
|
+
"engine": "OpenCode v2.1.4",
|
|
11
|
+
"requestedModel": "opencode/beam-free#default",
|
|
12
|
+
"fixtureCommit": "f486e57d1ba17d92e585b0b42dd4b9c71b2fda4a",
|
|
13
|
+
"sessionId": "ses_e8de7be3cffeBTghRaz6CN1T01",
|
|
14
|
+
"sessionOutcome": "failed",
|
|
15
|
+
"providerFailureType": "provider.free-limit",
|
|
16
|
+
"directSanitizedExportErrorTypes": [
|
|
17
|
+
"provider.free-limit",
|
|
18
|
+
"provider.free-limit"
|
|
19
|
+
],
|
|
20
|
+
"process": {
|
|
21
|
+
"shellPipelineExit": 1,
|
|
22
|
+
"opencodeProcessExit": null,
|
|
23
|
+
"opencodeProcessExitStatus": "UNKNOWN: pipeline status captured without PIPESTATUS"
|
|
24
|
+
},
|
|
25
|
+
"sanitizedExport": {
|
|
26
|
+
"exitCode": 0,
|
|
27
|
+
"messageCount": 4,
|
|
28
|
+
"sha256": "91f80a5e8b2c94015a5a5602022aa45436cff2a52295615f7eb022aed91eee2b",
|
|
29
|
+
"source": "LOCAL_ONLY:.worktrees/packets/resume-2026-10-25/D1-current/fixture/trace/native-export.json"
|
|
30
|
+
},
|
|
31
|
+
"fixture": {
|
|
32
|
+
"gitCleanBeforeAndAfter": true,
|
|
33
|
+
"configSha256": "422eb8c6481aaadbf7910d4b415e76b2e60723bd0da4c532a94fad1879c38bcd",
|
|
34
|
+
"adrBeforeSha256": "65c903eedc1be2ca6022638fd9cab1f70c08c278e9e03be5c73ad0b2ecab4d18",
|
|
35
|
+
"adrAfterSha256": "65c903eedc1be2ca6022638fd9cab1f70c08c278e9e03be5c73ad0b2ecab4d18",
|
|
36
|
+
"taskPromptSha256": "524e58eeb6bd64e33986777b19a7fca8ec0829154fc2eb4b0c4a26b8e13b7b5e"
|
|
37
|
+
},
|
|
38
|
+
"observations": {
|
|
39
|
+
"naturalCompletionObserved": false,
|
|
40
|
+
"observerLoadMarkerObserved": false,
|
|
41
|
+
"observerTracePresent": false,
|
|
42
|
+
"pilotReportPresent": false,
|
|
43
|
+
"innerConsumerCause": "UNCHECKED",
|
|
44
|
+
"globalHarnessAbsence": "UNCHECKED"
|
|
45
|
+
},
|
|
46
|
+
"sourceArtifactSha256": {
|
|
47
|
+
"beforeState": "91018ac09c0e58537b53ffbef6adb1d2ecd5cc5b079cccc60484493aa592d474",
|
|
48
|
+
"afterState": "d2f729d1f5eb29b6a17b552fb06a31833ecbe4ef3eda5096e895ebfab212518c",
|
|
49
|
+
"filteredRunLog": "b1f1b48237c94733a6753440617e52c85ee754d1287a78b94d2513e5fdfa07a5",
|
|
50
|
+
"observerImplementation": "33a3a3c8da4f5dc495f7462952b11ee8ad46c4fa9ba0882152acd0eebf0ed56f",
|
|
51
|
+
"taskLocalPluginEntrypoint": "ad4eef698164e644e52eeb0c6f36bef56ca75a5c66d7cd71345d0091108f8fbe"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"id": "D2",
|
|
56
|
+
"engine": "OpenCode CLI invocation; session identity UNKNOWN",
|
|
57
|
+
"requestedModel": "nvidia/deepseek-ai/deepseek-v4.1-flash",
|
|
58
|
+
"productBaseline": "37c97046cb1e620c4a3585b4da5a2593501a48be",
|
|
59
|
+
"fixtureCommit": "39aa08987e0da2fc2ff3ea3556b84bd6d37f4e79",
|
|
60
|
+
"nvidiaPreflight": {
|
|
61
|
+
"catalogModel": "nvidia/deepseek-ai/deepseek-v4.1-flash",
|
|
62
|
+
"configuredAuthPresence": true,
|
|
63
|
+
"variant": "UNKNOWN",
|
|
64
|
+
"evidenceLayer": "non-session preflight only",
|
|
65
|
+
"providerRequestMadeDuringPreflight": false
|
|
66
|
+
},
|
|
67
|
+
"process": {
|
|
68
|
+
"exitCode": 1,
|
|
69
|
+
"signal": null,
|
|
70
|
+
"timedOut": false,
|
|
71
|
+
"spawnFailureReason": null,
|
|
72
|
+
"stdoutJsonlRecords": 1,
|
|
73
|
+
"filteredEventType": "error",
|
|
74
|
+
"sessionId": null,
|
|
75
|
+
"sessionExistence": "UNKNOWN",
|
|
76
|
+
"stderrBytes": 0,
|
|
77
|
+
"rawOutputRetained": false
|
|
78
|
+
},
|
|
79
|
+
"errorInterpretation": {
|
|
80
|
+
"errorDiscriminantRetained": false,
|
|
81
|
+
"captureLimit": "allowlist omitted the error discriminant",
|
|
82
|
+
"cause": "UNKNOWN",
|
|
83
|
+
"providerRequestStatus": "UNKNOWN"
|
|
84
|
+
},
|
|
85
|
+
"fixture": {
|
|
86
|
+
"gitCleanBeforeAndAfter": true,
|
|
87
|
+
"configSha256": "422eb8c6481aaadbf7910d4b415e76b2e60723bd0da4c532a94fad1879c38bcd",
|
|
88
|
+
"adrBeforeSha256": "65c903eedc1be2ca6022638fd9cab1f70c08c278e9e03be5c73ad0b2ecab4d18",
|
|
89
|
+
"adrAfterSha256": "65c903eedc1be2ca6022638fd9cab1f70c08c278e9e03be5c73ad0b2ecab4d18",
|
|
90
|
+
"taskPromptSha256": "524e58eeb6bd64e33986777b19a7fca8ec0829154fc2eb4b0c4a26b8e13b7b5e",
|
|
91
|
+
"adrChanged": false
|
|
92
|
+
},
|
|
93
|
+
"observations": {
|
|
94
|
+
"naturalCompletionObserved": false,
|
|
95
|
+
"observerTracePresent": false,
|
|
96
|
+
"observerLoad": "NOT_OBSERVED",
|
|
97
|
+
"sanitizedExportAttempted": false,
|
|
98
|
+
"reportCapture": "UNKNOWN: no session ID was available for lookup",
|
|
99
|
+
"globalHarnessAbsence": "UNCHECKED",
|
|
100
|
+
"isolation": "UNCHECKED"
|
|
101
|
+
},
|
|
102
|
+
"sourceArtifactSha256": {
|
|
103
|
+
"exitMetadata": "61f8904e70fa9266f52437d536a43d9f8c34b338fa1c0f55827cf1527b7f7582",
|
|
104
|
+
"filteredRunLog": "b2261fe453f0645fb8d7606979e0c4a8b7c08095a6fc1fa3dbb9a670fbf78806",
|
|
105
|
+
"observerImplementation": "33a3a3c8da4f5dc495f7462952b11ee8ad46c4fa9ba0882152acd0eebf0ed56f",
|
|
106
|
+
"taskLocalPluginEntrypoint": "ad4eef698164e644e52eeb0c6f36bef56ca75a5c66d7cd71345d0091108f8fbe"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
"localRunnerSelfCheck": {
|
|
111
|
+
"result": "PASS",
|
|
112
|
+
"checks": [
|
|
113
|
+
"child exit code 23",
|
|
114
|
+
"timeout and signal capture",
|
|
115
|
+
"spawn failure maps to null exit plus reason"
|
|
116
|
+
],
|
|
117
|
+
"evidenceLayer": "deterministic mechanism only; not live-host evidence",
|
|
118
|
+
"runnerSource": "LOCAL_ONLY:.worktrees/packets/resume-2026-10-25/D2-nvidia-current/scripts/d2-runner.mjs"
|
|
119
|
+
},
|
|
120
|
+
"qualification": {
|
|
121
|
+
"e1Cycle": false,
|
|
122
|
+
"e1Go": false,
|
|
123
|
+
"behaviorBenefit": false,
|
|
124
|
+
"qualifies": false,
|
|
125
|
+
"nvidiaOutageClaim": false,
|
|
126
|
+
"prClaim": "retained negative observations only"
|
|
127
|
+
}
|
|
128
|
+
}
|
package/ci/action-gate.test.js
CHANGED
|
@@ -353,6 +353,92 @@ try {
|
|
|
353
353
|
|
|
354
354
|
check(fs.readFileSync(rulesPath, 'utf8') === fs.readFileSync(pluginRules, 'utf8'), 'canonical and plugin action-gate rule tables are byte-identical');
|
|
355
355
|
check(fs.existsSync(pluginScript), 'OpenAI plugin packages action-gate runtime');
|
|
356
|
+
|
|
357
|
+
// Codex PR metadata scope: exact operation AND actual argument allowlist.
|
|
358
|
+
const metadataInput = { repository_full_name: 'acme/repo', pr_number: 376, body: 'metadata fixture' };
|
|
359
|
+
const metadataPayload = (input = metadataInput, tool = 'mcp__codex_apps__github__update_pull_request', extra = {}) => ({
|
|
360
|
+
...payload('', { tool }), tool_input: input, ...extra,
|
|
361
|
+
});
|
|
362
|
+
for (const tool of ['update_pull_request', 'mcp__github__update_pull_request', 'mcp__codex_apps__github__update_pull_request', 'mcp__codex_apps__github_update_pull_request']) {
|
|
363
|
+
for (const fields of [{ body: '' }, { title: 'new title' }, { title: 'new title', body: 'new body' }]) {
|
|
364
|
+
const candidate = metadataPayload({ repository_full_name: 'acme/repo', pr_number: 376, ...fields }, tool);
|
|
365
|
+
const result = actionGate.evaluatePreToolUse(candidate, { host: 'codex', sessionDir, ruleTable: table });
|
|
366
|
+
check(result.kind === 'allow' && result.stdout === null && result.exitCode === 0, `${tool} ${Object.keys(fields)} defers without a permission decision`);
|
|
367
|
+
const descriptor = actionGate.normalizeActionDescriptor(candidate);
|
|
368
|
+
check(descriptor.action === 'mutate' && descriptor.classification === 'gated', 'metadata remains a non-safe mutation');
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
for (const selector of [{ owner: 'acme', repo: 'repo', pullNumber: 376 }, { owner: 'acme', repo: 'repo', pull_number: 376 }]) {
|
|
372
|
+
check(actionGate.evaluatePreToolUse(metadataPayload({ ...selector, body: 'provider-neutral' }), { host: 'codex', sessionDir, ruleTable: table }).kind === 'allow', 'provider-neutral selectors support metadata updates');
|
|
373
|
+
}
|
|
374
|
+
const ownUnknown = JSON.parse('{"repository_full_name":"acme/repo","pr_number":376,"body":"fixture","__proto__":{"state":"closed"}}');
|
|
375
|
+
check(actionGate.evaluatePreToolUse(metadataPayload(ownUnknown), { host: 'codex', sessionDir, ruleTable: table }).kind === 'block', 'own __proto__ field cannot escape the argument allowlist');
|
|
376
|
+
const explicitDelete = metadataPayload(metadataInput, undefined, { action_descriptor: { action: 'delete' } });
|
|
377
|
+
check(actionGate.evaluatePreToolUse(explicitDelete, { host: 'codex', sessionDir, ruleTable: table }).kind === 'block', 'explicit higher-risk action cannot inherit metadata deferral');
|
|
378
|
+
for (const extra of [
|
|
379
|
+
{ state: 'closed' }, { state: 'open' }, { state: null }, { base_branch: 'other' },
|
|
380
|
+
{ maintainer_can_modify: false }, { maintainer_can_modify: null }, { unknown: true },
|
|
381
|
+
{ title: 'mixed', state: 'closed' }, { draft: true }, { base: 'other' },
|
|
382
|
+
{ action_descriptor: { action: 'delete' } },
|
|
383
|
+
]) {
|
|
384
|
+
const result = actionGate.evaluatePreToolUse(metadataPayload({ ...metadataInput, ...extra }), { host: 'codex', sessionDir, ruleTable: table });
|
|
385
|
+
check(result.kind === 'block' && result.exitCode === 2, `metadata with ${JSON.stringify(extra)} remains gated`);
|
|
386
|
+
}
|
|
387
|
+
for (const input of [null, [], 'body', {}, { repository_full_name: 'acme/repo', pr_number: 376 },
|
|
388
|
+
{ ...metadataInput, body: null }, { ...metadataInput, title: 2 }, { ...metadataInput, body: {} }]) {
|
|
389
|
+
check(actionGate.evaluatePreToolUse(metadataPayload(input), { host: 'codex', sessionDir, ruleTable: table }).kind === 'block', 'missing or malformed metadata remains gated');
|
|
390
|
+
}
|
|
391
|
+
for (const tool of ['mcp__demo__update_pull_requests', 'mcp__demo__foo_update_pull_request', 'mcp__demo__update_pull_request_and_delete', 'mcp__codex_apps__github_update_pull_request_extra', 'mcp__demo__merge_pull_request', 'mcp__demo__close_pull_request']) {
|
|
392
|
+
check(actionGate.evaluatePreToolUse(metadataPayload(metadataInput, tool), { host: 'codex', sessionDir, ruleTable: table }).kind === 'block', `${tool} receives no metadata exception`);
|
|
393
|
+
}
|
|
394
|
+
for (const host of ['claude', 'opencode', 'unknown']) {
|
|
395
|
+
const result = actionGate.evaluatePreToolUse(metadataPayload(), { host, sessionDir, ruleTable: table });
|
|
396
|
+
check(result.rule && result.rule.id === 'structured-mutate', `${host} retains existing mutation rule`);
|
|
397
|
+
}
|
|
398
|
+
for (const hint of [{ actionGate: { required: true } }, { workflowPlan: { actionGate: { required: true } } }, { harness: { workflowPlan: { actionGate: { required: true } } } }, { routerContract: { workflowPlan: { actionGate: { required: true } } } }]) {
|
|
399
|
+
check(actionGate.evaluatePreToolUse(metadataPayload(metadataInput, undefined, hint), { host: 'codex', sessionDir, ruleTable: table }).kind === 'block', 'explicit required metadata gate cannot be bypassed');
|
|
400
|
+
}
|
|
401
|
+
for (const action of ['read', 'unknown']) {
|
|
402
|
+
for (const descriptor of [{ action_descriptor: { action } }, { actionDescriptor: { action } }, { harness: { actionDescriptor: { action } } }]) {
|
|
403
|
+
const candidate = metadataPayload({ ...metadataInput, state: 'closed' }, undefined, {
|
|
404
|
+
...descriptor, workflowPlan: { actionGate: { required: true } },
|
|
405
|
+
});
|
|
406
|
+
const result = actionGate.evaluatePreToolUse(candidate, { host: 'codex', sessionDir, ruleTable: table });
|
|
407
|
+
check(result.kind === 'block' && result.exitCode === 2, `known Codex PR mutation cannot be downgraded to ${action}`);
|
|
408
|
+
check(result.record && result.record.actionDescriptor.action === 'mutate', 'downgrade refusal remains an audited mutation');
|
|
409
|
+
}
|
|
410
|
+
const inputDescriptor = metadataPayload({ ...metadataInput, state: 'closed', action_descriptor: { action } }, undefined, { actionGate: { required: true } });
|
|
411
|
+
check(actionGate.evaluatePreToolUse(inputDescriptor, { host: 'codex', sessionDir, ruleTable: table }).kind === 'block', `input ${action} descriptor cannot hide a required PR mutation gate`);
|
|
412
|
+
}
|
|
413
|
+
const misleadingProvider = metadataPayload({ ...metadataInput, state: 'closed' }, 'mcp__get_provider__update_pull_request');
|
|
414
|
+
check(actionGate.evaluatePreToolUse(misleadingProvider, { host: 'codex', sessionDir, ruleTable: table }).kind === 'block', 'provider namespace read token cannot downgrade exact PR mutation');
|
|
415
|
+
for (const host of ['claude', 'opencode', 'unknown']) {
|
|
416
|
+
const describedRead = metadataPayload(metadataInput, undefined, { action_descriptor: { action: 'read' } });
|
|
417
|
+
check(actionGate.evaluatePreToolUse(describedRead, { host, sessionDir, ruleTable: table }).kind === 'allow', `${host} explicit descriptor behavior remains unchanged`);
|
|
418
|
+
}
|
|
419
|
+
for (const action of ['read', 'unknown', 'mutate', 'delete', 'publish', 'deploy', 'send']) {
|
|
420
|
+
const candidate = metadataPayload({ ...metadataInput, state: 'closed', action_descriptor: { toolFamily: 'shell', action } }, undefined, { actionGate: { required: true } });
|
|
421
|
+
const result = actionGate.evaluatePreToolUse(candidate, { host: 'codex', sessionDir, ruleTable: table });
|
|
422
|
+
check(result.kind === 'block' && result.exitCode === 2, `shell family cannot disguise exact Codex PR ${action}`);
|
|
423
|
+
check(result.record && result.record.actionDescriptor.toolFamily === 'mcp', 'known MCP identity is preserved in audit');
|
|
424
|
+
}
|
|
425
|
+
const gateEnvelopes = gate => [
|
|
426
|
+
{ actionGate: gate }, { workflowPlan: { actionGate: gate } },
|
|
427
|
+
{ routerContract: { workflowPlan: { actionGate: gate } } },
|
|
428
|
+
{ harness: { workflowPlan: { actionGate: gate } } },
|
|
429
|
+
];
|
|
430
|
+
for (let requiredAt = 0; requiredAt < 4; requiredAt++) {
|
|
431
|
+
const optionalHints = Object.assign({}, ...gateEnvelopes({ required: false }));
|
|
432
|
+
const hints = { ...optionalHints, ...gateEnvelopes({ required: true })[requiredAt] };
|
|
433
|
+
const candidate = metadataPayload(metadataInput, undefined, hints);
|
|
434
|
+
const result = actionGate.evaluatePreToolUse(candidate, { host: 'codex', sessionDir, ruleTable: table });
|
|
435
|
+
check(result.kind === 'block' && result.exitCode === 2, `required hint envelope ${requiredAt} wins over optional envelopes`);
|
|
436
|
+
check(result.record && result.record.routerActionGate.required === true, 'audit retains required hint instead of a conflicting optional hint');
|
|
437
|
+
const creation = { ...candidate, tool_name: 'mcp__github__create_pull_request' };
|
|
438
|
+
check(actionGate.evaluatePreToolUse(creation, { host: 'codex', sessionDir, ruleTable: table }).kind === 'block', 'PR creation still honors the documented explicit required gate');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
|
|
356
442
|
} catch (err) {
|
|
357
443
|
console.error(err.stack || err.message);
|
|
358
444
|
failed++;
|
|
@@ -127,6 +127,53 @@ try {
|
|
|
127
127
|
assert.strictEqual(attributedBlock.exitCode, 2);
|
|
128
128
|
assert.ok(attributedBlock.attribution.file && fs.existsSync(attributedBlock.attribution.file), 'destructive hook entry must be attributable before the block result');
|
|
129
129
|
|
|
130
|
+
// Execute both canonical and packaged pre-entrypoints with real MCP argument shapes.
|
|
131
|
+
for (const gate of [actionGate, require(path.join(pluginRoot, 'hooks', 'scripts', 'action-gate.js'))]) {
|
|
132
|
+
for (const outcome of ['success', 'failure']) {
|
|
133
|
+
const candidate = {
|
|
134
|
+
...payload('', { toolUseId: `metadata-${outcome}`, turnId: 'metadata-turn' }),
|
|
135
|
+
tool_name: 'mcp__codex_apps__github__update_pull_request',
|
|
136
|
+
tool_input: { repository_full_name: 'acme/repo', pr_number: 376, body: 'private fixture body' },
|
|
137
|
+
};
|
|
138
|
+
const pre = gate.evaluatePreToolUse(candidate, { host: 'codex', sessionDir, ruleTable: table });
|
|
139
|
+
assert.strictEqual(pre.kind, 'allow');
|
|
140
|
+
assert.strictEqual(pre.stdout, null);
|
|
141
|
+
assert.strictEqual(pre.record.disposition, 'deferred-to-host');
|
|
142
|
+
assert.strictEqual(pre.record.host, 'codex');
|
|
143
|
+
assert.strictEqual(pre.record.toolUseId, candidate.tool_use_id);
|
|
144
|
+
assert.strictEqual(pre.record.matchedRule, 'routine-pr-metadata-update');
|
|
145
|
+
const hash = gate.hashExact(gate.exactPayloadText(candidate));
|
|
146
|
+
assert.strictEqual(pre.record.commandHash, hash);
|
|
147
|
+
assert.strictEqual(pre.record.actionDescriptor.payloadHash, hash);
|
|
148
|
+
assert.ok(!JSON.stringify(pre.record).includes('private fixture body'));
|
|
149
|
+
const post = gate.finalizeExecution(candidate, outcome, { sessionDir });
|
|
150
|
+
assert.strictEqual(post.record.disposition, 'executed');
|
|
151
|
+
assert.strictEqual(post.record.executionOutcome, outcome);
|
|
152
|
+
assert.strictEqual(post.record.commandHash, hash);
|
|
153
|
+
|
|
154
|
+
gate.evaluatePreToolUse(candidate, { host: 'codex', sessionDir, ruleTable: table });
|
|
155
|
+
const altered = { ...candidate, tool_input: { ...candidate.tool_input, state: 'closed' } };
|
|
156
|
+
assert.strictEqual(gate.finalizeExecution(altered, 'success', { sessionDir }).record.disposition, 'integrity-violation');
|
|
157
|
+
const denied = gate.evaluatePreToolUse(altered, { host: 'codex', sessionDir, ruleTable: table });
|
|
158
|
+
assert.strictEqual(denied.kind, 'block');
|
|
159
|
+
assert.strictEqual(denied.exitCode, 2);
|
|
160
|
+
assert.strictEqual(denied.stdout, null);
|
|
161
|
+
assert.strictEqual(denied.record.disposition, 'rejected');
|
|
162
|
+
assert.strictEqual(denied.record.rejectionReason, 'host-ask-decision-unverified-or-unsupported');
|
|
163
|
+
assert.notStrictEqual(denied.record.commandHash, hash);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const metadataEntry = preAdapter.processPayload({
|
|
167
|
+
...payload('', { toolUseId: 'metadata-entry' }),
|
|
168
|
+
tool_name: 'mcp__codex_apps__github_update_pull_request',
|
|
169
|
+
tool_input: { repository_full_name: 'acme/repo', pr_number: 376, title: 'fixture' },
|
|
170
|
+
}, { host: 'codex', sessionDir, ruleTable: table, attributionDir, runNonce: 'metadata-fixture' });
|
|
171
|
+
assert.strictEqual(metadataEntry.kind, 'allow');
|
|
172
|
+
assert.strictEqual(metadataEntry.attribution.record.payloadHash, metadataEntry.record.commandHash);
|
|
173
|
+
assert.strictEqual(metadataEntry.attribution.record.toolUseId, metadataEntry.record.toolUseId);
|
|
174
|
+
assert.strictEqual(fs.readFileSync(path.join(ROOT, 'hooks/scripts/action-gate.js'), 'utf8'),
|
|
175
|
+
fs.readFileSync(path.join(pluginRoot, 'hooks/scripts/action-gate.js'), 'utf8'));
|
|
176
|
+
|
|
130
177
|
const blockedParent = path.join(tempRoot, 'not-a-directory');
|
|
131
178
|
fs.writeFileSync(blockedParent, 'fixture', 'utf8');
|
|
132
179
|
const failedAttribution = preAdapter.processPayload(payload('git push --force origin main', {
|
|
@@ -131,6 +131,18 @@ try {
|
|
|
131
131
|
}));
|
|
132
132
|
assert.strictEqual(neutralResult.status, 0, 'exact create_pull_request operation must be host-deferred on Codex (currently exits 2: RED)');
|
|
133
133
|
|
|
134
|
+
for (const tool of ['mcp__codex_apps__github__update_pull_request', 'mcp__codex_apps__github_update_pull_request', 'mcp__github__update_pull_request']) {
|
|
135
|
+
for (const fields of [{ body: 'replacement body' }, { title: 'replacement title' }, { title: 'both', body: '' }]) {
|
|
136
|
+
const result = run(payload(tool, { repository_full_name: 'acme/repo', pr_number: 1, ...fields }));
|
|
137
|
+
assert.strictEqual(result.status, 0, 'metadata-only actual stdin payload must be host-deferred');
|
|
138
|
+
assert.strictEqual(result.stdout.trim(), '', 'metadata deferral must emit no permission decision');
|
|
139
|
+
}
|
|
140
|
+
const denied = run(payload(tool, { repository_full_name: 'acme/repo', pr_number: 1, body: 'mixed', state: 'closed' }));
|
|
141
|
+
assert.strictEqual(denied.status, 2, 'mixed metadata/state actual stdin payload must fail closed');
|
|
142
|
+
assert.strictEqual(denied.stdout.trim(), '');
|
|
143
|
+
assert.match(denied.stderr, /structured-mutate/);
|
|
144
|
+
}
|
|
145
|
+
|
|
134
146
|
// Genuine mutations remain blocked, but the fixed-format approval protocol is retired.
|
|
135
147
|
const legacyMutation = run(payload('mcp__demo__update_issue', { issue_id: 1, title: 'changed' }));
|
|
136
148
|
assert.strictEqual(legacyMutation.status, 2, 'genuine mutation must remain blocked on Codex');
|
|
@@ -132,7 +132,8 @@ Harness does not parse, bind, or intercept any fixed approval syntax in user
|
|
|
132
132
|
prompts. Native agent task-scope authorization and native permissions remain
|
|
133
133
|
authoritative.
|
|
134
134
|
|
|
135
|
-
Routine exact `create_pull_request` operations
|
|
135
|
+
Routine exact `create_pull_request` operations and Codex title/body-only
|
|
136
|
+
`update_pull_request` operations are host-deferred (below). All
|
|
136
137
|
other risky actions (merge/delete/deploy/publish/send/force-push and unrelated
|
|
137
138
|
genuine mutations) remain hard PreToolUse blocks; an explicitly required
|
|
138
139
|
`actionGate` hint still gates. These are deterministic mechanism changes, not
|
|
@@ -153,3 +154,28 @@ risky actions (merge/delete/deploy/publish/send/force-push and unrelated genuine
|
|
|
153
154
|
mutations) remain gated; exact operation matching is used, with no substring
|
|
154
155
|
exemption, so misleading names such as `create_pull_requests` or
|
|
155
156
|
`foo_create_pull_request` still block.
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
## Codex PR title/body updates (host-deferred)
|
|
160
|
+
|
|
161
|
+
The Codex-only metadata exception checks the actual tool input, with all of these conditions:
|
|
162
|
+
|
|
163
|
+
- Exact final `__` operation `update_pull_request`, or exact flattened connector name `mcp__codex_apps__github_update_pull_request`.
|
|
164
|
+
- A non-array object containing at least one string `title` or `body` (empty body is supported).
|
|
165
|
+
- Only `title`, `body`, and selector keys `repository_full_name`, `pr_number`, `owner`, `repo`, `pullNumber`, `pull_number` are present.
|
|
166
|
+
- Exact Codex PR updates cannot be downgraded to `read`/`unknown` by an explicit descriptor or a provider namespace; higher-risk action descriptors remain gated. Their tool family comes from the actual tool identity, so a descriptor cannot disguise the MCP update as shell input.
|
|
167
|
+
- No explicit required `actionGate` hint in any supported envelope (a required hint wins over conflicting optional hints); the action descriptor remains `mutate`/`gated`.
|
|
168
|
+
|
|
169
|
+
Any additional key, including `state`, `base_branch`, `base`, `maintainer_can_modify`, `draft`, or an unknown key, prevents the exception, even when its value is null. Mixed metadata/high-risk inputs and similar operation names retain the existing Gate. Claude, OpenCode and unknown hosts retain their prior mutation rules. PR creation behavior is unchanged.
|
|
170
|
+
|
|
171
|
+
```mermaid
|
|
172
|
+
flowchart LR
|
|
173
|
+
A["Exact PR update + actual arguments"] --> B{"Codex + title/body only<br/>+ no required Gate?"}
|
|
174
|
+
B -->|Yes| C["Host-deferred mutation<br/>no permission decision"]
|
|
175
|
+
B -->|No| D["Existing structured Gate"]
|
|
176
|
+
C --> E["Native task scope + permissions<br/>+ GitHub authorization"]
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Host-deferred does not grant authorization. Harness emits no `permissionDecision` allow/ask; native permissions, task scope, and GitHub access still decide execution. The audit records `routine-pr-metadata-update`, `deferred-to-host`, host/tool provenance and the exact payload hash, without storing title/body. PostToolUse records execution success/failure; Stop rejects an unexecuted deferral. A changed payload cannot reuse the original hash.
|
|
180
|
+
|
|
181
|
+
Evidence: [RED/GREEN and independent review](../benchmarks/results/codex-pr-metadata-2026-10-26/README.md). Package/mechanism verification is separate from live Host verification (#110, #122, #135; affected PR #376).
|
|
@@ -6,7 +6,7 @@ status: active
|
|
|
6
6
|
# Document workflow reconciliation — Phase 3b S1 runtime
|
|
7
7
|
|
|
8
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
|
|
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 remains OPEN with insufficient qualifying exposure. The recorded OpenCode [D1/D2 attempts](../benchmarks/results/live-host/opencode-d1-d2-2026-10-25/README.md) do not qualify as E1 cycles.**
|
|
10
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
|
|
@@ -336,7 +336,7 @@ 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). With the pilot ON, F4 (#358) grants limited positive credit for `doc-lint` and `link-check` only; S3 provider integration is pending and
|
|
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; E1 remains OPEN with insufficient qualifying exposure, and D1/D2 are non-qualifying.**
|
|
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
342
|
orders Docs → RED → GREEN → independent review → green CI for each slice. S1 was the temporary
|
|
@@ -1085,7 +1085,8 @@ 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
|
|
1088
|
-
benefit. E1
|
|
1088
|
+
benefit. E1 observation remains OPEN with insufficient qualifying exposure; it begins only after
|
|
1089
|
+
a creditable S2, and the D1/D2 attempts are non-qualifying. E1 GO and H1–H4/paired evidence
|
|
1089
1090
|
remain separate gates.
|
|
1090
1091
|
|
|
1091
1092
|
## F4: credited reconciliation (Claude-only, pilot ON)
|
|
@@ -4,7 +4,7 @@ Contract for the documentation family: project docs configuration (D5), document
|
|
|
4
4
|
|
|
5
5
|
Documentation is a **consumer** of the #320 workflow/obligation abstractions. Nothing here is a second router, a standing maintenance loop or a cognitive lock: every check is a pure function that reports diagnostics.
|
|
6
6
|
|
|
7
|
-
## Scope of this slice (status
|
|
7
|
+
## Scope of this slice (original 2026-10-20 checkpoint; Phase 3b/3c status updated 2026-10-25)
|
|
8
8
|
|
|
9
9
|
This table distinguishes merged mechanism code from end-to-end workflow integration. The last reviewed `main` commit is `29b1d65b398e2622af97bf067078373b7fa76b26` (v0.36.2). That release commit carries `[skip ci]` and only bumps package/distribution versions on top of `e5d22584e243d5b5e587ee0279625e97a9033160`, whose full-gate `CI` run [39374474269](https://github.com/dyphn1/Harness-everything/actions/runs/39374474269) completed with every job succeeded. The historical Windows System One resident teardown (`S1-RS15`) was observed in attempt 1 of [run 39175240370](https://github.com/dyphn1/Harness-everything/actions/runs/39175240370) on `main@75e04359ae5bf11a41739f8eb04d25a76ee8f3a7`; that run completed successfully on attempt 2 and says nothing about `29b1d65`. Treat the resident teardown as a known platform limitation only when a current run on the exact SHA reproduces it, and require an all-green full-gate run for that SHA before calling any baseline green.
|
|
10
10
|
|
|
@@ -15,8 +15,8 @@ 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. **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
|
|
19
|
-
| 3c — H1–H4 and paired-task evaluation | **
|
|
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 remains OPEN with insufficient qualifying exposure, and D1/D2 are non-qualifying. Accepted #341 E0 permits this limited reversible consumer; the live runs are not E1 GO; preserve existing required-binding compatibility |
|
|
19
|
+
| 3c — H1–H4 and paired-task evaluation | **H1 preparation only** in [#373](https://github.com/dyphn1/Harness-everything/pull/373) ([retained evidence](../benchmarks/results/h1-provider-independence-qualification/README.md)); full H1–H4 and paired-task evaluation remain pending. 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
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
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
|
|
|
@@ -17,6 +17,10 @@ Statuses are intentionally explicit:
|
|
|
17
17
|
|
|
18
18
|
Codex and OpenCode are `Partial` in `liveHostVerification`. The Codex artifact verifies user-hook compatibility capture for two consecutive turns on one Windows Codex CLI host; native plugin-hook mounting and other host/version combinations remain unverified. OpenCode's retained evidence supports project-scope `.js` loading and edit/verification state on OpenCode 1.18.31 (macOS), not durable hard enforcement. No fresh artifacts exist yet for the other surfaces, managed-workspace import, or public OpenAI approval.
|
|
19
19
|
|
|
20
|
+
### Codex PR metadata action-gate boundary
|
|
21
|
+
|
|
22
|
+
Exact title/body-only `update_pull_request` calls are Codex-only host-deferred mutations after an argument allowlist check; explicit required Gate hints and all additional fields retain the existing Gate. Native task scope, permissions and GitHub authorization remain authoritative. Canonical/package mechanism tests cover hashes, audit provenance, outcomes and negative controls; this does not establish candidate live-host coverage. See [Codex policy](codex-action-gate.md#codex-pr-titlebody-updates-host-deferred).
|
|
23
|
+
|
|
20
24
|
### Routing contract vs. host evidence
|
|
21
25
|
|
|
22
26
|
The workflow plan controller rejects present non-array required/optional binding declarations before recording a requirement plan. This input-validation contract is covered by deterministic mechanism tests and does not change live-host evidence status.
|
|
@@ -27,7 +31,8 @@ This is a **repository/agent contract**, not an automatic upgrade to any platfor
|
|
|
27
31
|
|
|
28
32
|
### Action-gate mechanism vs. host semantic boundary
|
|
29
33
|
|
|
30
|
-
Routine exact `create_pull_request` operations
|
|
34
|
+
Routine exact `create_pull_request` operations and Codex title/body-only
|
|
35
|
+
`update_pull_request` operations are host-deferred at the mechanism level:
|
|
31
36
|
their descriptor remains a mutation (not read/safe), and no Harness consent or
|
|
32
37
|
`permissionDecision` is emitted. Native agent task-scope authorization and native
|
|
33
38
|
permissions stay authoritative; an explicit required `actionGate` hint still gates.
|
|
@@ -140,7 +145,8 @@ The public OpenAI Skills-only plugin is narrower again. Its ZIP contains the pac
|
|
|
140
145
|
Codex `UserPromptSubmit` prompts pass through to the normal router unchanged;
|
|
141
146
|
Harness does not parse or intercept a fixed approval syntax. Native agent
|
|
142
147
|
task-scope authorization and native permissions remain authoritative. Routine
|
|
143
|
-
exact `create_pull_request` operations
|
|
148
|
+
exact `create_pull_request` operations and Codex title/body-only
|
|
149
|
+
`update_pull_request` operations are host-deferred at the mechanism level:
|
|
144
150
|
their descriptor remains a mutation (not read/safe), and no Harness consent or
|
|
145
151
|
`permissionDecision` is emitted. All other risky actions remain hard PreToolUse
|
|
146
152
|
blocks; an explicitly required `actionGate` hint still gates. These are
|
|
@@ -108,12 +108,14 @@ function safeResource(input, explicit) {
|
|
|
108
108
|
return null;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
function normalizeActionDescriptor(payload) {
|
|
111
|
+
function normalizeActionDescriptor(payload, options = {}) {
|
|
112
112
|
const tool = toolNameOf(payload);
|
|
113
113
|
const input = toolInputOf(payload);
|
|
114
114
|
const explicit = explicitActionDescriptor(payload);
|
|
115
|
-
const
|
|
116
|
-
const
|
|
115
|
+
const codexPrUpdate = detectHost(payload, options.host) === 'codex' && isPullRequestUpdateTool(tool);
|
|
116
|
+
const toolFamily = inferToolFamily(tool, codexPrUpdate ? null : explicit);
|
|
117
|
+
const inferred = inferStructuredAction(tool, explicit);
|
|
118
|
+
const action = codexPrUpdate && !STRUCTURED_GATED_ACTIONS.has(inferred) ? 'mutate' : inferred;
|
|
117
119
|
const classification = STRUCTURED_GATED_ACTIONS.has(action)
|
|
118
120
|
? 'gated'
|
|
119
121
|
: (STRUCTURED_SAFE_ACTIONS.has(action) ? 'safe' : 'unknown');
|
|
@@ -140,8 +142,23 @@ function isRoutinePullRequestCreation(toolName) {
|
|
|
140
142
|
return (last === 'create_pull_request' || name === 'create_pull_request');
|
|
141
143
|
}
|
|
142
144
|
|
|
143
|
-
function
|
|
144
|
-
|
|
145
|
+
function isPullRequestUpdateTool(name) {
|
|
146
|
+
return name.split('__').pop() === 'update_pull_request' || name === 'mcp__codex_apps__github_update_pull_request';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isPullRequestMetadataUpdate(payload) {
|
|
150
|
+
if (!isPullRequestUpdateTool(toolNameOf(payload))) return false;
|
|
151
|
+
const input = toolInputOf(payload);
|
|
152
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return false;
|
|
153
|
+
const keys = Object.keys(input);
|
|
154
|
+
const allowed = new Set(['repository_full_name', 'pr_number', 'owner', 'repo', 'pullNumber', 'pull_number', 'title', 'body']);
|
|
155
|
+
return keys.every(key => allowed.has(key))
|
|
156
|
+
&& keys.some(key => key === 'title' || key === 'body')
|
|
157
|
+
&& keys.filter(key => key === 'title' || key === 'body').every(key => typeof input[key] === 'string');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function structuredRule(payload, options = {}) {
|
|
161
|
+
const descriptor = normalizeActionDescriptor(payload, options);
|
|
145
162
|
if (descriptor.toolFamily === 'shell') return { descriptor, rule: null };
|
|
146
163
|
if (descriptor.classification === 'safe') return { descriptor, rule: null };
|
|
147
164
|
// Routine PR creation is a host-deferred mutation, not a Harness-gated one:
|
|
@@ -152,6 +169,10 @@ function structuredRule(payload) {
|
|
|
152
169
|
if (isRoutinePullRequestCreation(descriptor.toolName) && !(hint && hint.required)) {
|
|
153
170
|
return { descriptor, rule: null };
|
|
154
171
|
}
|
|
172
|
+
if (detectHost(payload, options.host) === 'codex' && descriptor.action === 'mutate'
|
|
173
|
+
&& isPullRequestMetadataUpdate(payload) && !(hint && hint.required)) {
|
|
174
|
+
return { descriptor, rule: null, hostDeferred: true };
|
|
175
|
+
}
|
|
155
176
|
if (descriptor.classification === 'gated') {
|
|
156
177
|
return {
|
|
157
178
|
descriptor,
|
|
@@ -367,7 +388,8 @@ function routerActionGateHint(payload) {
|
|
|
367
388
|
payload && payload.routerContract && payload.routerContract.workflowPlan && payload.routerContract.workflowPlan.actionGate,
|
|
368
389
|
payload && payload.harness && payload.harness.workflowPlan && payload.harness.workflowPlan.actionGate,
|
|
369
390
|
];
|
|
370
|
-
const gate = candidates.find(candidate => candidate && typeof candidate === 'object')
|
|
391
|
+
const gate = candidates.find(candidate => candidate && typeof candidate === 'object' && candidate.required)
|
|
392
|
+
|| candidates.find(candidate => candidate && typeof candidate === 'object');
|
|
371
393
|
if (!gate) return null;
|
|
372
394
|
return {
|
|
373
395
|
required: Boolean(gate.required),
|
|
@@ -435,7 +457,7 @@ function recordDecision(payload, rule, table, host, disposition, options = {}, e
|
|
|
435
457
|
const exact = exactPayloadText(payload);
|
|
436
458
|
const commandHash = hashExact(exact);
|
|
437
459
|
const now = new Date().toISOString();
|
|
438
|
-
const descriptor = normalizeActionDescriptor(payload);
|
|
460
|
+
const descriptor = normalizeActionDescriptor(payload, { host });
|
|
439
461
|
const record = {
|
|
440
462
|
schemaVersion: 1,
|
|
441
463
|
toolName: toolNameOf(payload),
|
|
@@ -467,11 +489,15 @@ function recordDecision(payload, rule, table, host, disposition, options = {}, e
|
|
|
467
489
|
function evaluatePreToolUse(payload, options = {}) {
|
|
468
490
|
const context = sessionContext(payload, options.sessionDir);
|
|
469
491
|
const table = options.ruleTable || loadRuleTable(options.rulePath || RULES_PATH);
|
|
470
|
-
const structured = structuredRule(payload);
|
|
492
|
+
const structured = structuredRule(payload, options);
|
|
471
493
|
const rule = structured.rule || classify(payload, table.rules, context.sessionDir);
|
|
472
494
|
const host = detectHost(payload, options.host);
|
|
473
495
|
|
|
474
496
|
if (!rule) {
|
|
497
|
+
const record = structured.hostDeferred ? recordDecision(payload, {
|
|
498
|
+
id: 'routine-pr-metadata-update',
|
|
499
|
+
reason: 'Codex PR title/body metadata update defers to native task scope and permissions.',
|
|
500
|
+
}, table, host, 'deferred-to-host', { ...options, sessionDir: context.sessionDir }) : null;
|
|
475
501
|
return {
|
|
476
502
|
kind: 'allow',
|
|
477
503
|
exitCode: 0,
|
|
@@ -480,6 +506,7 @@ function evaluatePreToolUse(payload, options = {}) {
|
|
|
480
506
|
rule: null,
|
|
481
507
|
table,
|
|
482
508
|
descriptor: structured.descriptor,
|
|
509
|
+
...(record ? { record } : {}),
|
|
483
510
|
};
|
|
484
511
|
}
|
|
485
512
|
|
package/package.json
CHANGED