harness-everything 0.41.2 → 0.41.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +6 -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 +28 -21
- package/docs/platform-capabilities.md +20 -16
- package/hooks/scripts/action-gate.js +18 -47
- package/hooks/scripts/codex-user-prompt.js +4 -16
- 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 +18 -47
- package/plugins/harness-everything/hooks/scripts/codex-user-prompt.js +4 -16
- package/plugins/harness-everything/plugin.json +1 -1
- package/sbom.cdx.json +7 -7
- package/ci/mechanism-codex-action-authorization.test.js +0 -292
- package/hooks/scripts/lib/codex-action-authorization.js +0 -193
- package/plugins/harness-everything/hooks/scripts/lib/codex-action-authorization.js +0 -193
|
@@ -1,193 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,193 +0,0 @@
|
|
|
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
|
-
};
|