harness-everything 0.41.0 → 0.41.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,193 @@
1
+ 'use strict';
2
+
3
+ // Cooperative Codex explicit-authorization state. This is NOT tamperproof:
4
+ // it is local hook state keyed by host session. A local process or user can
5
+ // delete or rewrite it. It records consent only; it never bypasses Codex's
6
+ // native permission controls.
7
+ const crypto = require('crypto');
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const { getWorkspaceRoot, getSessionDir } = require('./harness-state');
12
+
13
+ const DIR_NAME = 'codex-action-authorization';
14
+ const CHALLENGES = 'challenges';
15
+ const GRANTS = 'grants';
16
+
17
+ function sessionDirOf(payload, options) {
18
+ if (options && options.sessionDir) return options.sessionDir;
19
+ const root = (options && options.root) || getWorkspaceRoot(payload);
20
+ const sessionId = payload && (payload.session_id || payload.sessionId);
21
+ return getSessionDir(root, sessionId, payload);
22
+ }
23
+
24
+ function safeToken(value) {
25
+ return String(value || 'x').replace(/[^A-Za-z0-9._-]+/g, '_').slice(0, 160) || 'x';
26
+ }
27
+
28
+ function authDir(payload, options, leaf) {
29
+ return path.join(sessionDirOf(payload, options), DIR_NAME, leaf);
30
+ }
31
+
32
+ function challengesDir(payload, options) { return authDir(payload, options, CHALLENGES); }
33
+ function grantsDir(payload, options) { return authDir(payload, options, GRANTS); }
34
+
35
+ function ensureDir(dir) { fs.mkdirSync(dir, { recursive: true }); }
36
+
37
+ function readJson(file) {
38
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_) { return null; }
39
+ }
40
+
41
+ function atomicWriteJson(filePath, value) {
42
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
43
+ const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
44
+ fs.writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
45
+ fs.renameSync(temp, filePath);
46
+ }
47
+
48
+ function newChallengeId() {
49
+ return `ch-${crypto.randomBytes(12).toString('hex')}`;
50
+ }
51
+
52
+ // Canonical challenge ids are minted by this helper as ch-<24 hex>. A pending
53
+ // grant is stored as exactly <challengeId>.json. Anything with an extra suffix
54
+ // (.consumed, .consumed.json, etc.) is a claim/consumed record, not pending.
55
+ const CANONICAL_CHALLENGE_ID = /^ch-[0-9a-f]{24}$/;
56
+ function isPendingGrantFileName(name) {
57
+ if (typeof name !== 'string') return false;
58
+ const match = name.match(/^([^/\\]+)\.json$/);
59
+ if (!match) return false;
60
+ return CANONICAL_CHALLENGE_ID.test(match[1]);
61
+ }
62
+
63
+ function toolNameOf(payload) {
64
+ return String((payload && (payload.tool_name || payload.toolName)) || '');
65
+ }
66
+
67
+ function validIdentity(value) {
68
+ return typeof value === 'string' && value.trim().length > 0;
69
+ }
70
+
71
+ function actionGateModule() {
72
+ try { return require(path.join(__dirname, '..', 'action-gate.js')); } catch (_) { return null; }
73
+ }
74
+
75
+ function canonicalDigest(payload) {
76
+ const ag = actionGateModule();
77
+ if (ag && typeof ag.exactPayloadText === 'function' && typeof ag.hashExact === 'function') {
78
+ return ag.hashExact(ag.exactPayloadText(payload));
79
+ }
80
+ const input = (payload && (payload.tool_input || payload.toolInput || payload.input)) || {};
81
+ return crypto.createHash('sha256').update(JSON.stringify(input), 'utf8').digest('hex');
82
+ }
83
+
84
+ function issueChallenge(payload, options) {
85
+ options = options || {};
86
+ const challengeId = options.challengeId || newChallengeId();
87
+ const dir = challengesDir(payload, options);
88
+ ensureDir(dir);
89
+ const file = path.join(dir, `${safeToken(challengeId)}.json`);
90
+ const record = {
91
+ schemaVersion: 1,
92
+ challengeId,
93
+ sessionId: payload && (payload.session_id || payload.sessionId) || null,
94
+ toolName: options.toolName || toolNameOf(payload),
95
+ digest: options.digest || canonicalDigest(payload),
96
+ createdAt: new Date().toISOString(),
97
+ };
98
+ atomicWriteJson(file, record);
99
+ return challengeId;
100
+ }
101
+
102
+ function bindConsent(payload, options) {
103
+ options = options || {};
104
+ const challengeId = options.challengeId;
105
+ if (!challengeId) return { bound: false, reason: 'missing-challenge-id' };
106
+
107
+ const dir = challengesDir(payload, options);
108
+ const file = path.join(dir, `${safeToken(challengeId)}.json`);
109
+ const challenge = readJson(file);
110
+ if (!challenge) return { bound: false, reason: 'challenge-not-found' };
111
+
112
+ const sessionId = payload && (payload.session_id || payload.sessionId);
113
+ if (challenge.sessionId && challenge.sessionId !== sessionId) {
114
+ return { bound: false, reason: 'session-mismatch' };
115
+ }
116
+
117
+ const consentTurn = payload && (payload.turn_id || payload.turnId);
118
+ if (!validIdentity(sessionId) || !validIdentity(consentTurn)) {
119
+ return { bound: false, reason: 'missing-identity' };
120
+ }
121
+ const gdir = grantsDir(payload, options);
122
+ ensureDir(gdir);
123
+ const grantFile = path.join(gdir, `${safeToken(challengeId)}.json`);
124
+ const grant = {
125
+ schemaVersion: 1,
126
+ challengeId,
127
+ sessionId,
128
+ consentTurn: consentTurn || null,
129
+ toolName: challenge.toolName,
130
+ digest: challenge.digest,
131
+ createdAt: new Date().toISOString(),
132
+ consumed: false,
133
+ };
134
+ atomicWriteJson(grantFile, grant);
135
+ return { bound: true, challenge };
136
+ }
137
+
138
+ function consumeGrantIfValid(payload, options) {
139
+ options = options || {};
140
+ const sessionId = payload && (payload.session_id || payload.sessionId);
141
+ const turnId = payload && (payload.turn_id || payload.turnId);
142
+ if (!validIdentity(sessionId) || !validIdentity(turnId)) {
143
+ return { authorized: false };
144
+ }
145
+ const toolName = toolNameOf(payload);
146
+ const digest = canonicalDigest(payload);
147
+
148
+ const dir = grantsDir(payload, options);
149
+ let files = [];
150
+ try { files = fs.readdirSync(dir).filter(isPendingGrantFileName); } catch (_) { files = []; }
151
+
152
+ for (const name of files) {
153
+ const file = path.join(dir, name);
154
+ const grant = readJson(file);
155
+ if (!grant || grant.consumed) continue;
156
+ if (grant.sessionId !== sessionId) continue;
157
+ if (grant.consentTurn !== turnId) continue; // cross-turn / expired
158
+ if (grant.toolName !== toolName) continue;
159
+ if (grant.digest !== digest) continue;
160
+
161
+ // Atomic consume: first rename wins.
162
+ const consumedFile = path.join(dir, `${path.basename(name, '.json')}.consumed`);
163
+ try {
164
+ fs.renameSync(file, consumedFile);
165
+ } catch (_) {
166
+ continue; // lost race or already consumed
167
+ }
168
+ grant.consumed = true;
169
+ grant.consumedAt = new Date().toISOString();
170
+ try { atomicWriteJson(consumedFile, grant); } catch (_) { /* best effort */ }
171
+ return { authorized: true };
172
+ }
173
+ return { authorized: false };
174
+ }
175
+
176
+ function isExactConsentPrompt(prompt) {
177
+ return /^HARNESS APPROVE [A-Za-z0-9._-]{8,}$/.test(String(prompt || ''));
178
+ }
179
+
180
+ function parseConsentPrompt(prompt) {
181
+ const match = String(prompt || '').match(/^HARNESS APPROVE ([A-Za-z0-9._-]{8,})$/);
182
+ return match ? match[1] : null;
183
+ }
184
+
185
+ module.exports = {
186
+ bindConsent,
187
+ consumeGrantIfValid,
188
+ issueChallenge,
189
+ isExactConsentPrompt,
190
+ parseConsentPrompt,
191
+ canonicalDigest,
192
+ toolNameOf,
193
+ };
@@ -5,6 +5,30 @@ const { getWorkspaceRoot, getSessionDir } = require('./lib/harness-state');
5
5
  const { cwdOf, classifyShell, isVerificationShell, directMutationInWorkspace } = require('./lib/workflow-isolation');
6
6
  const { observeTool } = require('./lib/telemetry');
7
7
 
8
+ function isSuccessfulMetadataStep(command, cwd) {
9
+ // A line break starts a second shell command, so it can never be metadata-only.
10
+ if (/[\r\n]/.test(String(command || ''))) return false;
11
+ const text = String(command || '').trim();
12
+ if (/[$`%!^]/.test(text)) return false;
13
+ const value = '(?:"[^"\\r\\n]*"|\'[^\'\\r\\n]*\'|[^\\s;&|<>`$(){}]+)';
14
+ const pattern = new RegExp(
15
+ `^node\\s+(?<script>${value})\\s+step\\s+--session-id\\s+(?<session>${value})` +
16
+ `\\s+--step-id\\s+(?<step>${value})\\s+--disposition\\s+(?<disposition>pass|blocked)` +
17
+ `(?:\\s+--reason-code\\s+(?<reason>${value}))?\\s+--evidence\\s+(?<evidence>${value})$`
18
+ );
19
+ const match = pattern.exec(text);
20
+ if (!match) return false;
21
+ const unquote = token => token && token.length >= 2 &&
22
+ ((token[0] === '"' && token[token.length - 1] === '"') || (token[0] === "'" && token[token.length - 1] === "'"))
23
+ ? token.slice(1, -1) : token || '';
24
+ const { script, session, step, disposition, reason, evidence } = match.groups;
25
+ return path.resolve(cwd, unquote(script)) === path.join(__dirname, 'workflow-disposition.js')
26
+ && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(unquote(session))
27
+ && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(unquote(step))
28
+ && Boolean(unquote(evidence).trim())
29
+ && (disposition === 'pass' ? reason === undefined : /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(unquote(reason)));
30
+ }
31
+
8
32
  // S2b doc-reconciliation pilot capture (docs/doc-workflow-reconciliation.md
9
33
  // § S2b "Activation and pilot-only scope", "Claude capture and origin-provider
10
34
  // contract"): pilot-only, fail-open to the host, and gated on the exact
@@ -75,7 +99,9 @@ function processState(payload) {
75
99
  const command = (payload.tool_input && payload.tool_input.command) || '';
76
100
  if (isShell) {
77
101
  const commandClass = classifyShell(command);
78
- if (commandClass !== 'read-only' && commandClass !== 'worktree-setup') state.lastEditAt = Date.now();
102
+ const metadataStep = !isFailed && (isSuccessEvent || exitCode === 0)
103
+ && isSuccessfulMetadataStep(command, cwdOf(payload, root));
104
+ if (!metadataStep && commandClass !== 'read-only' && commandClass !== 'worktree-setup') state.lastEditAt = Date.now();
79
105
  // Verification evidence needs a confirmed success: a PostToolUse event or an explicit zero exit.
80
106
  // Pilot ON only: a registered, argv-validated doc-check that resolves to this install's own runtime is the document
81
107
  // verification evidence, and only at the shipped pin (R3-02). Any other path earns no verification evidence. OFF stays legacy.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "harness-enforcement",
3
3
  "description": "Repository inventory for the opencode enforcement plugin and its standalone helper",
4
- "version": "0.41.0",
4
+ "version": "0.41.2",
5
5
  "runtime": "index.mjs",
6
6
  "hooks": {
7
7
  "verify": "hooks/verify.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.41.0",
3
+ "version": "0.41.2",
4
4
  "description": "Harness OS: An Orchestrated Agent Operating System for AI-driven Development",
5
5
  "main": "harness-everything/scripts/bootstrap.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.41.0",
3
+ "version": "0.41.2",
4
4
  "description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
5
5
  "author": {
6
6
  "name": "Miya Daniel",
@@ -9,6 +9,7 @@ const {
9
9
  getWorkspaceRoot,
10
10
  getSessionDir,
11
11
  } = require('./lib/harness-state');
12
+ const codexActionAuthorization = require('./lib/codex-action-authorization');
12
13
 
13
14
  const RULES_PATH = process.env.HARNESS_ACTION_GATE_RULES_PATH || path.join(__dirname, 'action-gate-rules.json');
14
15
  const ASK_CAPABLE_HOSTS = new Set(['claude']);
@@ -52,10 +53,12 @@ function toolInputOf(payload) {
52
53
  function stableValue(value) {
53
54
  if (Array.isArray(value)) return value.map(stableValue);
54
55
  if (!value || typeof value !== 'object') return value;
56
+ // Use a null-prototype accumulator so an own `__proto__` key is stored as data
57
+ // instead of mutating the accumulator's prototype (which would drop it).
55
58
  return Object.keys(value).sort().reduce((acc, key) => {
56
59
  acc[key] = stableValue(value[key]);
57
60
  return acc;
58
- }, {});
61
+ }, Object.create(null));
59
62
  }
60
63
 
61
64
  function exactPayloadText(payload) {
@@ -445,11 +448,45 @@ function recordDecision(payload, rule, table, host, disposition, options = {}, e
445
448
  return record;
446
449
  }
447
450
 
451
+
452
+ function isScopedCodexStructuredMutation(structured, rule) {
453
+ return Boolean(
454
+ rule && rule.id === 'structured-mutate' &&
455
+ structured && structured.descriptor &&
456
+ structured.descriptor.toolFamily !== 'shell' &&
457
+ structured.descriptor.action === 'mutate' &&
458
+ structured.descriptor.classification === 'gated'
459
+ );
460
+ }
461
+
462
+ function safeInline(value) {
463
+ return String(value == null ? '' : value).replace(/[\r\n\t\u0000-\u001f\u007f]/g, '?').slice(0, 200);
464
+ }
465
+
448
466
  function evaluatePreToolUse(payload, options = {}) {
449
467
  const context = sessionContext(payload, options.sessionDir);
450
468
  const table = options.ruleTable || loadRuleTable(options.rulePath || RULES_PATH);
451
469
  const structured = structuredRule(payload);
452
470
  const rule = structured.rule || classify(payload, table.rules, context.sessionDir);
471
+ const host = detectHost(payload, options.host);
472
+
473
+ // Codex explicit authorization applies only to scoped structured mutations.
474
+ // Shell-governed and other structured actions keep ordinary blocking.
475
+ if (host === 'codex' && isScopedCodexStructuredMutation(structured, rule)) {
476
+ const consumed = codexActionAuthorization.consumeGrantIfValid(payload, { sessionDir: context.sessionDir });
477
+ if (consumed && consumed.authorized) {
478
+ return {
479
+ kind: 'allow',
480
+ exitCode: 0,
481
+ stdout: null,
482
+ stderr: null,
483
+ rule: null,
484
+ table,
485
+ descriptor: structured.descriptor,
486
+ };
487
+ }
488
+ }
489
+
453
490
  if (!rule) {
454
491
  return {
455
492
  kind: 'allow',
@@ -462,7 +499,6 @@ function evaluatePreToolUse(payload, options = {}) {
462
499
  };
463
500
  }
464
501
 
465
- const host = detectHost(payload, options.host);
466
502
  const reason = decisionReason(rule, table);
467
503
  if (hostSupportsAsk(host) && gatePolicy(options) === ALWAYS_ASK_POLICY) {
468
504
  const record = recordDecision(payload, rule, table, host, 'pending-approval', { ...options, sessionDir: context.sessionDir });
@@ -478,11 +514,25 @@ function evaluatePreToolUse(payload, options = {}) {
478
514
  const record = recordDecision(payload, rule, table, host, 'rejected', { ...options, sessionDir: context.sessionDir }, {
479
515
  rejectionReason: 'host-ask-decision-unverified-or-unsupported',
480
516
  });
517
+ let challengeSuffix = '';
518
+ let detailsSuffix = '';
519
+ if (host === 'codex' && isScopedCodexStructuredMutation(structured, rule)) {
520
+ const challengeId = codexActionAuthorization.issueChallenge(payload, {
521
+ sessionDir: context.sessionDir,
522
+ toolName: toolNameOf(payload),
523
+ digest: (structured.descriptor && structured.descriptor.payloadHash) || hashExact(exactPayloadText(payload)),
524
+ });
525
+ if (challengeId) {
526
+ challengeSuffix = ` challenge id: ${challengeId}`;
527
+ const digest = (structured.descriptor && structured.descriptor.payloadHash) || hashExact(exactPayloadText(payload));
528
+ detailsSuffix = ` Tool: ${safeInline(toolNameOf(payload))}. Input SHA-256: ${digest}. Approve with: HARNESS APPROVE ${challengeId}.`;
529
+ }
530
+ }
481
531
  return {
482
532
  kind: 'block',
483
533
  exitCode: 2,
484
534
  stdout: null,
485
- stderr: `${reason} Host '${host}' has no Harness-verified ask-style decision path, so the action is blocked.`,
535
+ stderr: `${reason} Host '${host}' has no Harness-verified ask-style decision path, so the action is blocked.${challengeSuffix}${detailsSuffix}`,
486
536
  rule,
487
537
  table,
488
538
  record,
@@ -22,6 +22,26 @@ 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
+ // Codex explicit authorization: only an exact host-submitted consent prompt
30
+ // may bind a pending challenge to the host session + consent turn. No
31
+ // natural-language inference, no tool-input claim.
32
+ if (payload && payload.hook_event_name === 'UserPromptSubmit') {
33
+ const prompt = payload.prompt;
34
+ const match = String(prompt || '').match(/^HARNESS APPROVE ([A-Za-z0-9._-]{8,})$/);
35
+ if (match) {
36
+ try {
37
+ const auth = require('./lib/codex-action-authorization');
38
+ auth.bindConsent(payload, { challengeId: match[1] });
39
+ } catch (_) { /* consent state is best-effort; failures do not authorize */ }
40
+ process.exit(0);
41
+ return;
42
+ }
43
+ }
44
+
25
45
  if (!ROUTER) process.exit(0);
26
46
  const result = spawnSync(process.execPath, [ROUTER], { input, encoding: 'utf8', env: process.env, timeout: 25000 });
27
47
  const stderr = String(result.stderr || '');
@@ -0,0 +1,193 @@
1
+ 'use strict';
2
+
3
+ // Cooperative Codex explicit-authorization state. This is NOT tamperproof:
4
+ // it is local hook state keyed by host session. A local process or user can
5
+ // delete or rewrite it. It records consent only; it never bypasses Codex's
6
+ // native permission controls.
7
+ const crypto = require('crypto');
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const { getWorkspaceRoot, getSessionDir } = require('./harness-state');
12
+
13
+ const DIR_NAME = 'codex-action-authorization';
14
+ const CHALLENGES = 'challenges';
15
+ const GRANTS = 'grants';
16
+
17
+ function sessionDirOf(payload, options) {
18
+ if (options && options.sessionDir) return options.sessionDir;
19
+ const root = (options && options.root) || getWorkspaceRoot(payload);
20
+ const sessionId = payload && (payload.session_id || payload.sessionId);
21
+ return getSessionDir(root, sessionId, payload);
22
+ }
23
+
24
+ function safeToken(value) {
25
+ return String(value || 'x').replace(/[^A-Za-z0-9._-]+/g, '_').slice(0, 160) || 'x';
26
+ }
27
+
28
+ function authDir(payload, options, leaf) {
29
+ return path.join(sessionDirOf(payload, options), DIR_NAME, leaf);
30
+ }
31
+
32
+ function challengesDir(payload, options) { return authDir(payload, options, CHALLENGES); }
33
+ function grantsDir(payload, options) { return authDir(payload, options, GRANTS); }
34
+
35
+ function ensureDir(dir) { fs.mkdirSync(dir, { recursive: true }); }
36
+
37
+ function readJson(file) {
38
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_) { return null; }
39
+ }
40
+
41
+ function atomicWriteJson(filePath, value) {
42
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
43
+ const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
44
+ fs.writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
45
+ fs.renameSync(temp, filePath);
46
+ }
47
+
48
+ function newChallengeId() {
49
+ return `ch-${crypto.randomBytes(12).toString('hex')}`;
50
+ }
51
+
52
+ // Canonical challenge ids are minted by this helper as ch-<24 hex>. A pending
53
+ // grant is stored as exactly <challengeId>.json. Anything with an extra suffix
54
+ // (.consumed, .consumed.json, etc.) is a claim/consumed record, not pending.
55
+ const CANONICAL_CHALLENGE_ID = /^ch-[0-9a-f]{24}$/;
56
+ function isPendingGrantFileName(name) {
57
+ if (typeof name !== 'string') return false;
58
+ const match = name.match(/^([^/\\]+)\.json$/);
59
+ if (!match) return false;
60
+ return CANONICAL_CHALLENGE_ID.test(match[1]);
61
+ }
62
+
63
+ function toolNameOf(payload) {
64
+ return String((payload && (payload.tool_name || payload.toolName)) || '');
65
+ }
66
+
67
+ function validIdentity(value) {
68
+ return typeof value === 'string' && value.trim().length > 0;
69
+ }
70
+
71
+ function actionGateModule() {
72
+ try { return require(path.join(__dirname, '..', 'action-gate.js')); } catch (_) { return null; }
73
+ }
74
+
75
+ function canonicalDigest(payload) {
76
+ const ag = actionGateModule();
77
+ if (ag && typeof ag.exactPayloadText === 'function' && typeof ag.hashExact === 'function') {
78
+ return ag.hashExact(ag.exactPayloadText(payload));
79
+ }
80
+ const input = (payload && (payload.tool_input || payload.toolInput || payload.input)) || {};
81
+ return crypto.createHash('sha256').update(JSON.stringify(input), 'utf8').digest('hex');
82
+ }
83
+
84
+ function issueChallenge(payload, options) {
85
+ options = options || {};
86
+ const challengeId = options.challengeId || newChallengeId();
87
+ const dir = challengesDir(payload, options);
88
+ ensureDir(dir);
89
+ const file = path.join(dir, `${safeToken(challengeId)}.json`);
90
+ const record = {
91
+ schemaVersion: 1,
92
+ challengeId,
93
+ sessionId: payload && (payload.session_id || payload.sessionId) || null,
94
+ toolName: options.toolName || toolNameOf(payload),
95
+ digest: options.digest || canonicalDigest(payload),
96
+ createdAt: new Date().toISOString(),
97
+ };
98
+ atomicWriteJson(file, record);
99
+ return challengeId;
100
+ }
101
+
102
+ function bindConsent(payload, options) {
103
+ options = options || {};
104
+ const challengeId = options.challengeId;
105
+ if (!challengeId) return { bound: false, reason: 'missing-challenge-id' };
106
+
107
+ const dir = challengesDir(payload, options);
108
+ const file = path.join(dir, `${safeToken(challengeId)}.json`);
109
+ const challenge = readJson(file);
110
+ if (!challenge) return { bound: false, reason: 'challenge-not-found' };
111
+
112
+ const sessionId = payload && (payload.session_id || payload.sessionId);
113
+ if (challenge.sessionId && challenge.sessionId !== sessionId) {
114
+ return { bound: false, reason: 'session-mismatch' };
115
+ }
116
+
117
+ const consentTurn = payload && (payload.turn_id || payload.turnId);
118
+ if (!validIdentity(sessionId) || !validIdentity(consentTurn)) {
119
+ return { bound: false, reason: 'missing-identity' };
120
+ }
121
+ const gdir = grantsDir(payload, options);
122
+ ensureDir(gdir);
123
+ const grantFile = path.join(gdir, `${safeToken(challengeId)}.json`);
124
+ const grant = {
125
+ schemaVersion: 1,
126
+ challengeId,
127
+ sessionId,
128
+ consentTurn: consentTurn || null,
129
+ toolName: challenge.toolName,
130
+ digest: challenge.digest,
131
+ createdAt: new Date().toISOString(),
132
+ consumed: false,
133
+ };
134
+ atomicWriteJson(grantFile, grant);
135
+ return { bound: true, challenge };
136
+ }
137
+
138
+ function consumeGrantIfValid(payload, options) {
139
+ options = options || {};
140
+ const sessionId = payload && (payload.session_id || payload.sessionId);
141
+ const turnId = payload && (payload.turn_id || payload.turnId);
142
+ if (!validIdentity(sessionId) || !validIdentity(turnId)) {
143
+ return { authorized: false };
144
+ }
145
+ const toolName = toolNameOf(payload);
146
+ const digest = canonicalDigest(payload);
147
+
148
+ const dir = grantsDir(payload, options);
149
+ let files = [];
150
+ try { files = fs.readdirSync(dir).filter(isPendingGrantFileName); } catch (_) { files = []; }
151
+
152
+ for (const name of files) {
153
+ const file = path.join(dir, name);
154
+ const grant = readJson(file);
155
+ if (!grant || grant.consumed) continue;
156
+ if (grant.sessionId !== sessionId) continue;
157
+ if (grant.consentTurn !== turnId) continue; // cross-turn / expired
158
+ if (grant.toolName !== toolName) continue;
159
+ if (grant.digest !== digest) continue;
160
+
161
+ // Atomic consume: first rename wins.
162
+ const consumedFile = path.join(dir, `${path.basename(name, '.json')}.consumed`);
163
+ try {
164
+ fs.renameSync(file, consumedFile);
165
+ } catch (_) {
166
+ continue; // lost race or already consumed
167
+ }
168
+ grant.consumed = true;
169
+ grant.consumedAt = new Date().toISOString();
170
+ try { atomicWriteJson(consumedFile, grant); } catch (_) { /* best effort */ }
171
+ return { authorized: true };
172
+ }
173
+ return { authorized: false };
174
+ }
175
+
176
+ function isExactConsentPrompt(prompt) {
177
+ return /^HARNESS APPROVE [A-Za-z0-9._-]{8,}$/.test(String(prompt || ''));
178
+ }
179
+
180
+ function parseConsentPrompt(prompt) {
181
+ const match = String(prompt || '').match(/^HARNESS APPROVE ([A-Za-z0-9._-]{8,})$/);
182
+ return match ? match[1] : null;
183
+ }
184
+
185
+ module.exports = {
186
+ bindConsent,
187
+ consumeGrantIfValid,
188
+ issueChallenge,
189
+ isExactConsentPrompt,
190
+ parseConsentPrompt,
191
+ canonicalDigest,
192
+ toolNameOf,
193
+ };
@@ -5,6 +5,30 @@ const { getWorkspaceRoot, getSessionDir } = require('./lib/harness-state');
5
5
  const { cwdOf, classifyShell, isVerificationShell, directMutationInWorkspace } = require('./lib/workflow-isolation');
6
6
  const { observeTool } = require('./lib/telemetry');
7
7
 
8
+ function isSuccessfulMetadataStep(command, cwd) {
9
+ // A line break starts a second shell command, so it can never be metadata-only.
10
+ if (/[\r\n]/.test(String(command || ''))) return false;
11
+ const text = String(command || '').trim();
12
+ if (/[$`%!^]/.test(text)) return false;
13
+ const value = '(?:"[^"\\r\\n]*"|\'[^\'\\r\\n]*\'|[^\\s;&|<>`$(){}]+)';
14
+ const pattern = new RegExp(
15
+ `^node\\s+(?<script>${value})\\s+step\\s+--session-id\\s+(?<session>${value})` +
16
+ `\\s+--step-id\\s+(?<step>${value})\\s+--disposition\\s+(?<disposition>pass|blocked)` +
17
+ `(?:\\s+--reason-code\\s+(?<reason>${value}))?\\s+--evidence\\s+(?<evidence>${value})$`
18
+ );
19
+ const match = pattern.exec(text);
20
+ if (!match) return false;
21
+ const unquote = token => token && token.length >= 2 &&
22
+ ((token[0] === '"' && token[token.length - 1] === '"') || (token[0] === "'" && token[token.length - 1] === "'"))
23
+ ? token.slice(1, -1) : token || '';
24
+ const { script, session, step, disposition, reason, evidence } = match.groups;
25
+ return path.resolve(cwd, unquote(script)) === path.join(__dirname, 'workflow-disposition.js')
26
+ && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(unquote(session))
27
+ && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(unquote(step))
28
+ && Boolean(unquote(evidence).trim())
29
+ && (disposition === 'pass' ? reason === undefined : /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(unquote(reason)));
30
+ }
31
+
8
32
  // S2b doc-reconciliation pilot capture (docs/doc-workflow-reconciliation.md
9
33
  // § S2b "Activation and pilot-only scope", "Claude capture and origin-provider
10
34
  // contract"): pilot-only, fail-open to the host, and gated on the exact
@@ -75,7 +99,9 @@ function processState(payload) {
75
99
  const command = (payload.tool_input && payload.tool_input.command) || '';
76
100
  if (isShell) {
77
101
  const commandClass = classifyShell(command);
78
- if (commandClass !== 'read-only' && commandClass !== 'worktree-setup') state.lastEditAt = Date.now();
102
+ const metadataStep = !isFailed && (isSuccessEvent || exitCode === 0)
103
+ && isSuccessfulMetadataStep(command, cwdOf(payload, root));
104
+ if (!metadataStep && commandClass !== 'read-only' && commandClass !== 'worktree-setup') state.lastEditAt = Date.now();
79
105
  // Verification evidence needs a confirmed success: a PostToolUse event or an explicit zero exit.
80
106
  // Pilot ON only: a registered, argv-validated doc-check that resolves to this install's own runtime is the document
81
107
  // verification evidence, and only at the shipped pin (R3-02). Any other path earns no verification evidence. OFF stays legacy.
@@ -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.0",
4
+ "version": "0.41.2",
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:aa55b54e-8557-5a93-860d-8137bcfa2772",
5
+ "serialNumber": "urn:uuid:7e234a8b-69cb-55df-a4b5-86c44515fd0c",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-10-23T18:32:45.895Z",
8
+ "timestamp": "2026-10-25T03:28:58.892Z",
9
9
  "tools": [
10
10
  {
11
11
  "vendor": "Harness Everything",
12
12
  "name": "generate-sbom.js",
13
- "version": "0.41.0"
13
+ "version": "0.41.2"
14
14
  }
15
15
  ],
16
16
  "component": {
17
17
  "type": "application",
18
- "bom-ref": "pkg:npm/harness-everything@0.41.0",
18
+ "bom-ref": "pkg:npm/harness-everything@0.41.2",
19
19
  "name": "harness-everything",
20
- "version": "0.41.0",
21
- "purl": "pkg:npm/harness-everything@0.41.0"
20
+ "version": "0.41.2",
21
+ "purl": "pkg:npm/harness-everything@0.41.2"
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.0",
2036
+ "ref": "pkg:npm/harness-everything@0.41.2",
2037
2037
  "dependsOn": [
2038
2038
  "pkg:npm/%40commitlint/cli@19.8.1",
2039
2039
  "pkg:npm/%40commitlint/config-conventional@19.8.1",