@guilz-dev/belay 0.8.0 → 0.9.0
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/README.md +60 -10
- package/dist/adapters/shared/gate-runtime.d.ts +5 -0
- package/dist/adapters/shared/gate-runtime.js +237 -33
- package/dist/bundle/claude-runtime.mjs +4082 -1230
- package/dist/bundle/codex-runtime.mjs +4057 -1205
- package/dist/bundle/cursor-runtime.mjs +4057 -1205
- package/dist/commands/classify-for-report.js +5 -0
- package/dist/commands/doctor.js +90 -27
- package/dist/commands/explain.js +3 -2
- package/dist/commands/metrics.js +75 -4
- package/dist/commands/quality.js +2 -6
- package/dist/commands/recovery-checkpoints.d.ts +8 -1
- package/dist/commands/recovery-checkpoints.js +44 -11
- package/dist/commands/session.d.ts +5 -0
- package/dist/commands/session.js +8 -1
- package/dist/commands/status.js +1 -1
- package/dist/config-io.js +2 -2
- package/dist/conformance/contained-execution-guarantee.d.ts +121 -0
- package/dist/conformance/contained-execution-guarantee.js +121 -0
- package/dist/conformance/guarantee-table.js +7 -0
- package/dist/core/audit-io.js +8 -0
- package/dist/core/audit-metrics.d.ts +34 -0
- package/dist/core/audit-metrics.js +87 -15
- package/dist/core/audit-recovery-metrics.d.ts +33 -0
- package/dist/core/audit-recovery-metrics.js +146 -0
- package/dist/core/audit-types.d.ts +7 -1
- package/dist/core/audit-types.js +1 -1
- package/dist/core/bounded-output.d.ts +11 -0
- package/dist/core/bounded-output.js +26 -0
- package/dist/core/capability/attestation.d.ts +56 -0
- package/dist/core/capability/attestation.js +110 -3
- package/dist/core/capability/boundary-session.d.ts +5 -0
- package/dist/core/capability/boundary-session.js +38 -1
- package/dist/core/capability/index.d.ts +1 -1
- package/dist/core/capability/index.js +1 -1
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.js +89 -10
- package/dist/core/contained-execution/docker-policy.d.ts +18 -0
- package/dist/core/contained-execution/docker-policy.js +97 -0
- package/dist/core/contained-execution/docker.d.ts +223 -0
- package/dist/core/contained-execution/docker.js +846 -0
- package/dist/core/contained-execution/eligibility.d.ts +9 -0
- package/dist/core/contained-execution/eligibility.js +103 -0
- package/dist/core/contained-execution/failure.d.ts +18 -0
- package/dist/core/contained-execution/failure.js +105 -0
- package/dist/core/contained-execution/mirror.d.ts +54 -0
- package/dist/core/contained-execution/mirror.js +587 -0
- package/dist/core/contained-execution/policy.d.ts +10 -0
- package/dist/core/contained-execution/policy.js +20 -0
- package/dist/core/effect-ir/index.d.ts +1 -1
- package/dist/core/effect-ir/index.js +1 -1
- package/dist/core/effect-ir/normalize.d.ts +5 -0
- package/dist/core/effect-ir/normalize.js +17 -7
- package/dist/core/effect-ir/shell-build.js +2 -14
- package/dist/core/effect-ir/shell-lower.js +100 -25
- package/dist/core/gate-contract.d.ts +4 -1
- package/dist/core/gate-contract.js +2 -0
- package/dist/core/process-runner.d.ts +10 -3
- package/dist/core/process-runner.js +66 -10
- package/dist/core/recovery/checkpoint.d.ts +3 -0
- package/dist/core/recovery/checkpoint.js +33 -4
- package/dist/core/recovery/types.d.ts +1 -0
- package/dist/core/scrub.d.ts +17 -0
- package/dist/core/scrub.js +499 -5
- package/dist/core/standing-allow.d.ts +0 -20
- package/dist/core/standing-allow.js +0 -71
- package/dist/core/transactional/apply-observed-changes.d.ts +1 -0
- package/dist/core/transactional/apply-observed-changes.js +11 -0
- package/dist/core/transactional/backend-selector.d.ts +2 -1
- package/dist/core/transactional/backend-selector.js +32 -29
- package/dist/core/transactional/backend.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-backend.js +242 -54
- package/dist/core/transactional/file-checkpoint-git.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-git.js +53 -0
- package/dist/core/transactional/file-checkpoint-isolation.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-isolation.js +16 -0
- package/dist/core/transactional/git-worktree-backend.js +3 -0
- package/dist/core/transactional/git-worktree.d.ts +1 -0
- package/dist/core/transactional/git-worktree.js +1 -0
- package/dist/core/transactional/runner.js +59 -13
- package/dist/core/types.d.ts +13 -0
- package/dist/core/verdict/parser.d.ts +10 -0
- package/dist/core/verdict/parser.js +271 -40
- package/dist/core/verdict/shell-semantics.js +21 -0
- package/dist/corpus/adversarial-probe.d.ts +1 -1
- package/dist/corpus/adversarial-probe.js +3 -3
- package/dist/corpus/benign-probe-cores.d.ts +6 -0
- package/dist/corpus/{must-allow-commands.js → benign-probe-cores.js} +8 -2
- package/dist/corpus/coverage-compare.d.ts +36 -0
- package/dist/corpus/coverage-compare.js +197 -0
- package/dist/corpus/coverage-contexts.d.ts +17 -0
- package/dist/corpus/coverage-contexts.js +88 -0
- package/dist/corpus/coverage-matrix.d.ts +37 -0
- package/dist/corpus/coverage-matrix.js +159 -0
- package/dist/corpus/coverage-probe.d.ts +91 -0
- package/dist/corpus/coverage-probe.js +341 -0
- package/dist/corpus/evaluate.d.ts +1 -1
- package/dist/corpus/evaluate.js +1 -1
- package/dist/corpus/structural-fixture-root.d.ts +2 -0
- package/dist/corpus/structural-fixture-root.js +5 -0
- package/dist/operational-insights.d.ts +3 -0
- package/dist/operational-insights.js +7 -3
- package/dist/runtime-provenance.d.ts +8 -0
- package/dist/runtime-provenance.js +34 -0
- package/dist/types.d.ts +3 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -1
- package/skills/belay/SKILL.md +19 -0
- package/skills/belay/belay-status.md +5 -0
- package/dist/corpus/must-allow-commands.d.ts +0 -6
package/dist/core/scrub.js
CHANGED
|
@@ -1,15 +1,34 @@
|
|
|
1
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
2
|
+
const AUTHORIZATION_SCHEMES = ['bearer', 'basic', 'token'];
|
|
3
|
+
const GENERIC_AUTH_HEADERS = ['X-Api-Key', 'X-Auth-Token', 'Private-Token'];
|
|
4
|
+
const KEY_VALUE_SECRET_NAMES = [
|
|
5
|
+
'api_key',
|
|
6
|
+
'api-key',
|
|
7
|
+
'apikey',
|
|
8
|
+
'token',
|
|
9
|
+
'secret',
|
|
10
|
+
'password',
|
|
11
|
+
'passwd',
|
|
12
|
+
'credential',
|
|
13
|
+
];
|
|
14
|
+
function regexAlternation(values) {
|
|
15
|
+
return values.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
|
16
|
+
}
|
|
17
|
+
const AUTHORIZATION_SCHEME_ALTERNATION = regexAlternation(AUTHORIZATION_SCHEMES);
|
|
18
|
+
const GENERIC_AUTH_HEADER_ALTERNATION = regexAlternation(GENERIC_AUTH_HEADERS);
|
|
19
|
+
const KEY_VALUE_SECRET_NAME_ALTERNATION = regexAlternation(KEY_VALUE_SECRET_NAMES);
|
|
1
20
|
const UUID_PATTERN = /\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/gi;
|
|
2
21
|
const TIMESTAMP_PATTERN = /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\b/g;
|
|
3
22
|
const APPROVAL_ID_PATTERN = /\bbelay_[a-z0-9]{8,}\b/gi;
|
|
4
23
|
const TOKEN_PREFIX_PATTERN = /\/belay-approve\s+\S+/gi;
|
|
5
24
|
const BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}\b/gi;
|
|
6
|
-
const AUTH_HEADER_PATTERN =
|
|
25
|
+
const AUTH_HEADER_PATTERN = new RegExp(`(?<!["'])\\bAuthorization:\\s*(?:${AUTHORIZATION_SCHEME_ALTERNATION})?\\s*\\S+`, 'gi');
|
|
7
26
|
const DOUBLE_QUOTED_AUTH_HEADER_PATTERN = /"Authorization:\s*[^"]*"/gi;
|
|
8
27
|
const SINGLE_QUOTED_AUTH_HEADER_PATTERN = /'Authorization:\s*[^']*'/gi;
|
|
9
|
-
const GENERIC_AUTH_HEADER_PATTERN =
|
|
10
|
-
const DOUBLE_QUOTED_GENERIC_AUTH_HEADER_PATTERN =
|
|
11
|
-
const SINGLE_QUOTED_GENERIC_AUTH_HEADER_PATTERN =
|
|
12
|
-
const KEY_VALUE_SECRET_PATTERN =
|
|
28
|
+
const GENERIC_AUTH_HEADER_PATTERN = new RegExp(`(?<!["'])\\b(?:${GENERIC_AUTH_HEADER_ALTERNATION}):\\s*\\S+`, 'gi');
|
|
29
|
+
const DOUBLE_QUOTED_GENERIC_AUTH_HEADER_PATTERN = new RegExp(`"(${GENERIC_AUTH_HEADER_ALTERNATION}):\\s*[^"]*"`, 'gi');
|
|
30
|
+
const SINGLE_QUOTED_GENERIC_AUTH_HEADER_PATTERN = new RegExp(`'(${GENERIC_AUTH_HEADER_ALTERNATION}):\\s*[^']*'`, 'gi');
|
|
31
|
+
const KEY_VALUE_SECRET_PATTERN = new RegExp(`\\b(${KEY_VALUE_SECRET_NAME_ALTERNATION})\\b\\s*[:=]\\s*['"]?[^\\s'"]{4,}`, 'gi');
|
|
13
32
|
const URL_CREDENTIALS_PATTERN = /\b([A-Za-z][A-Za-z0-9+.-]*:\/\/)([^/\s:@]+):([^@\s/]+)@/g;
|
|
14
33
|
const MYSQL_INLINE_PASSWORD_PATTERN = /(\s-p)([^\s]+)/g;
|
|
15
34
|
const HIGH_ENTROPY_PATTERN = /\b[A-Za-z0-9+/]{40,}={0,2}\b/g;
|
|
@@ -69,6 +88,481 @@ export function scrubString(value, options = {}) {
|
|
|
69
88
|
}
|
|
70
89
|
return scrubbed;
|
|
71
90
|
}
|
|
91
|
+
export const STREAMING_SCRUBBER_MAX_BUFFER_BYTES = 256;
|
|
92
|
+
const UUID_SHAPE = [
|
|
93
|
+
...Array(8).fill('hex'),
|
|
94
|
+
'-',
|
|
95
|
+
...Array(4).fill('hex'),
|
|
96
|
+
'-',
|
|
97
|
+
'version',
|
|
98
|
+
...Array(3).fill('hex'),
|
|
99
|
+
'-',
|
|
100
|
+
'variant',
|
|
101
|
+
...Array(3).fill('hex'),
|
|
102
|
+
'-',
|
|
103
|
+
...Array(12).fill('hex'),
|
|
104
|
+
];
|
|
105
|
+
const TIMESTAMP_SHAPE = [
|
|
106
|
+
'digit',
|
|
107
|
+
'digit',
|
|
108
|
+
'digit',
|
|
109
|
+
'digit',
|
|
110
|
+
'-',
|
|
111
|
+
'digit',
|
|
112
|
+
'digit',
|
|
113
|
+
'-',
|
|
114
|
+
'digit',
|
|
115
|
+
'digit',
|
|
116
|
+
'T',
|
|
117
|
+
'digit',
|
|
118
|
+
'digit',
|
|
119
|
+
':',
|
|
120
|
+
'digit',
|
|
121
|
+
'digit',
|
|
122
|
+
':',
|
|
123
|
+
'digit',
|
|
124
|
+
'digit',
|
|
125
|
+
];
|
|
126
|
+
function isWordCharacter(character) {
|
|
127
|
+
return character !== undefined && /[A-Za-z0-9_]/.test(character);
|
|
128
|
+
}
|
|
129
|
+
function matchesShapeCharacter(character, expected) {
|
|
130
|
+
if (expected === 'hex')
|
|
131
|
+
return /[0-9a-f]/i.test(character);
|
|
132
|
+
if (expected === 'digit')
|
|
133
|
+
return /\d/.test(character);
|
|
134
|
+
if (expected === 'version')
|
|
135
|
+
return /[1-5]/.test(character);
|
|
136
|
+
if (expected === 'variant')
|
|
137
|
+
return /[89ab]/i.test(character);
|
|
138
|
+
return character === expected;
|
|
139
|
+
}
|
|
140
|
+
function shapePrefixMatches(value, shape) {
|
|
141
|
+
const length = Math.min(value.length, shape.length);
|
|
142
|
+
for (let index = 0; index < length; index += 1) {
|
|
143
|
+
const character = value[index];
|
|
144
|
+
const expected = shape[index];
|
|
145
|
+
if (!character || !expected || !matchesShapeCharacter(character, expected))
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
function configuredMarkers(options) {
|
|
151
|
+
const markers = [];
|
|
152
|
+
if (options.maskAuthHeaders) {
|
|
153
|
+
for (const quote of ['"', "'"]) {
|
|
154
|
+
markers.push({
|
|
155
|
+
kind: 'quoted_authorization',
|
|
156
|
+
literal: `${quote}Authorization:`,
|
|
157
|
+
quote,
|
|
158
|
+
});
|
|
159
|
+
for (const header of GENERIC_AUTH_HEADERS) {
|
|
160
|
+
markers.push({
|
|
161
|
+
kind: 'quoted_generic_header',
|
|
162
|
+
literal: `${quote}${header}:`,
|
|
163
|
+
quote,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
markers.push({
|
|
168
|
+
kind: 'authorization',
|
|
169
|
+
literal: 'Authorization:',
|
|
170
|
+
requiresWordBoundary: true,
|
|
171
|
+
});
|
|
172
|
+
for (const header of GENERIC_AUTH_HEADERS) {
|
|
173
|
+
markers.push({ kind: 'generic_header', literal: `${header}:`, requiresWordBoundary: true });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (options.maskBearerTokens) {
|
|
177
|
+
markers.push({ kind: 'bearer', literal: 'Bearer', requiresWordBoundary: true });
|
|
178
|
+
}
|
|
179
|
+
if (options.maskKeyValueSecrets) {
|
|
180
|
+
for (const name of KEY_VALUE_SECRET_NAMES) {
|
|
181
|
+
markers.push({ kind: 'key_name', literal: name, requiresWordBoundary: true });
|
|
182
|
+
}
|
|
183
|
+
markers.push({ kind: 'url_authority', literal: '://' });
|
|
184
|
+
}
|
|
185
|
+
if (options.maskApprovalIds) {
|
|
186
|
+
markers.push({ kind: 'approval_command', literal: '/belay-approve' }, { kind: 'approval_id', literal: 'belay_', requiresWordBoundary: true });
|
|
187
|
+
}
|
|
188
|
+
return markers;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Creates a deterministic scrubber for unbounded process output.
|
|
192
|
+
*
|
|
193
|
+
* Only bounded, undecided grammar prefixes are retained. Once a bounded sensitive marker is
|
|
194
|
+
* recognized, the scrubber enters a suppression state before consuming any unbounded whitespace,
|
|
195
|
+
* credential value, or URL authority. URL authorities are conservatively hidden in full because a
|
|
196
|
+
* streaming parser cannot know whether an arbitrarily long username will later be followed by a
|
|
197
|
+
* password separator without retaining it.
|
|
198
|
+
*/
|
|
199
|
+
export function createStreamingScrubber(options = {}) {
|
|
200
|
+
const resolved = resolvedScrubOptions(options);
|
|
201
|
+
const markers = configuredMarkers(resolved);
|
|
202
|
+
const decoder = new StringDecoder('utf8');
|
|
203
|
+
let pending = '';
|
|
204
|
+
let previousRawCharacter;
|
|
205
|
+
let state;
|
|
206
|
+
const consumeRaw = (length) => {
|
|
207
|
+
const consumed = pending.slice(0, length);
|
|
208
|
+
pending = pending.slice(length);
|
|
209
|
+
if (consumed)
|
|
210
|
+
previousRawCharacter = consumed.at(-1);
|
|
211
|
+
return consumed;
|
|
212
|
+
};
|
|
213
|
+
const markerAtStart = () => {
|
|
214
|
+
const lowerPending = pending.toLowerCase();
|
|
215
|
+
let awaiting = false;
|
|
216
|
+
for (const candidate of markers) {
|
|
217
|
+
if (candidate.requiresWordBoundary && isWordCharacter(previousRawCharacter))
|
|
218
|
+
continue;
|
|
219
|
+
const lowerLiteral = candidate.literal.toLowerCase();
|
|
220
|
+
if (lowerLiteral.startsWith(lowerPending) && pending.length < candidate.literal.length) {
|
|
221
|
+
awaiting = true;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (lowerPending.startsWith(lowerLiteral)) {
|
|
225
|
+
return {
|
|
226
|
+
kind: candidate.kind,
|
|
227
|
+
length: candidate.literal.length,
|
|
228
|
+
quote: candidate.quote,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (resolved.maskKeyValueSecrets && /^\s$/.test(pending[0] ?? '')) {
|
|
233
|
+
if (pending.length === 1)
|
|
234
|
+
return 'awaiting';
|
|
235
|
+
if (pending[1] === '-' && pending.length === 2)
|
|
236
|
+
return 'awaiting';
|
|
237
|
+
if (pending[1] === '-' && pending[2]?.toLowerCase() === 'p') {
|
|
238
|
+
return { kind: 'mysql_password', length: 3 };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return awaiting ? 'awaiting' : undefined;
|
|
242
|
+
};
|
|
243
|
+
const uuidAtStart = (final) => {
|
|
244
|
+
if (isWordCharacter(previousRawCharacter) || !/[0-9a-f]/i.test(pending[0] ?? ''))
|
|
245
|
+
return;
|
|
246
|
+
if (!shapePrefixMatches(pending, UUID_SHAPE))
|
|
247
|
+
return;
|
|
248
|
+
if (pending.length < UUID_SHAPE.length)
|
|
249
|
+
return 'awaiting';
|
|
250
|
+
if (pending.length === UUID_SHAPE.length && !final)
|
|
251
|
+
return 'awaiting';
|
|
252
|
+
return !isWordCharacter(pending[UUID_SHAPE.length]) ? 'complete' : undefined;
|
|
253
|
+
};
|
|
254
|
+
const timestampAtStart = () => {
|
|
255
|
+
if (isWordCharacter(previousRawCharacter) || !/\d/.test(pending[0] ?? ''))
|
|
256
|
+
return;
|
|
257
|
+
if (!shapePrefixMatches(pending, TIMESTAMP_SHAPE))
|
|
258
|
+
return;
|
|
259
|
+
return pending.length < TIMESTAMP_SHAPE.length ? 'awaiting' : 'complete';
|
|
260
|
+
};
|
|
261
|
+
const highEntropyAtStart = () => {
|
|
262
|
+
if (!resolved.maskHighEntropyStrings || isWordCharacter(previousRawCharacter))
|
|
263
|
+
return;
|
|
264
|
+
if (!/[A-Za-z0-9]/.test(pending[0] ?? ''))
|
|
265
|
+
return;
|
|
266
|
+
let runLength = 0;
|
|
267
|
+
while (runLength < pending.length && /[A-Za-z0-9+/]/.test(pending[runLength] ?? '')) {
|
|
268
|
+
runLength += 1;
|
|
269
|
+
}
|
|
270
|
+
if (runLength >= 40)
|
|
271
|
+
return 'complete';
|
|
272
|
+
return runLength === pending.length ? 'awaiting' : undefined;
|
|
273
|
+
};
|
|
274
|
+
const drain = (final) => {
|
|
275
|
+
let output = '';
|
|
276
|
+
while (pending) {
|
|
277
|
+
if (state?.kind === 'quoted_value') {
|
|
278
|
+
const end = pending.indexOf(state.quote);
|
|
279
|
+
if (end < 0) {
|
|
280
|
+
consumeRaw(pending.length);
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
consumeRaw(end);
|
|
284
|
+
output += consumeRaw(1);
|
|
285
|
+
state = undefined;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (state?.kind === 'key_separator') {
|
|
289
|
+
const character = pending[0] ?? '';
|
|
290
|
+
if (/\s/.test(character)) {
|
|
291
|
+
output += consumeRaw(1);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (character === ':' || character === '=') {
|
|
295
|
+
output += `${consumeRaw(1)}<redacted>`;
|
|
296
|
+
state = { kind: 'key_value', phase: 'leading' };
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
state = undefined;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (state?.kind === 'bearer_wait' || state?.kind === 'approval_wait') {
|
|
303
|
+
const character = pending[0] ?? '';
|
|
304
|
+
if (/\s/.test(character)) {
|
|
305
|
+
consumeRaw(1);
|
|
306
|
+
output += state.kind === 'bearer_wait' ? ' <redacted>' : ' <approval-id>';
|
|
307
|
+
state =
|
|
308
|
+
state.kind === 'bearer_wait'
|
|
309
|
+
? { kind: 'bearer_value', phase: 'leading' }
|
|
310
|
+
: { kind: 'non_whitespace_value', phase: 'leading' };
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
state = undefined;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (state?.kind === 'key_value') {
|
|
317
|
+
const character = pending[0] ?? '';
|
|
318
|
+
if (state.phase === 'leading') {
|
|
319
|
+
if (/\s/.test(character)) {
|
|
320
|
+
consumeRaw(1);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (character === '"' || character === "'") {
|
|
324
|
+
output += consumeRaw(1);
|
|
325
|
+
state = { kind: 'quoted_value', quote: character };
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
state = { kind: 'key_value', phase: 'unquoted' };
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (/[\s'"]/.test(character)) {
|
|
332
|
+
output += consumeRaw(1);
|
|
333
|
+
state = undefined;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
consumeRaw(1);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (state?.kind === 'non_whitespace_value') {
|
|
340
|
+
const character = pending[0] ?? '';
|
|
341
|
+
if (state.phase === 'leading') {
|
|
342
|
+
if (/\s/.test(character)) {
|
|
343
|
+
consumeRaw(1);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
state = { kind: 'non_whitespace_value', phase: 'value' };
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (/\s/.test(character)) {
|
|
350
|
+
output += consumeRaw(1);
|
|
351
|
+
state = undefined;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
consumeRaw(1);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (state?.kind === 'bearer_value') {
|
|
358
|
+
const character = pending[0] ?? '';
|
|
359
|
+
if (state.phase === 'leading') {
|
|
360
|
+
if (/\s/.test(character)) {
|
|
361
|
+
consumeRaw(1);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (!/[A-Za-z0-9._~+/=-]/.test(character)) {
|
|
365
|
+
state = undefined;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
state = { kind: 'bearer_value', phase: 'value' };
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (!/[A-Za-z0-9._~+/=-]/.test(character)) {
|
|
372
|
+
state = undefined;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
consumeRaw(1);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (state?.kind === 'authorization_value') {
|
|
379
|
+
const character = pending[0] ?? '';
|
|
380
|
+
if (state.phase === 'leading' || state.phase === 'after_scheme') {
|
|
381
|
+
if (/\s/.test(character)) {
|
|
382
|
+
consumeRaw(1);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
state =
|
|
386
|
+
state.phase === 'leading'
|
|
387
|
+
? { kind: 'authorization_value', phase: 'first_word', firstWord: '' }
|
|
388
|
+
: { kind: 'authorization_value', phase: 'credential', firstWord: '' };
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (state.phase === 'first_word') {
|
|
392
|
+
if (/\s/.test(character)) {
|
|
393
|
+
const isScheme = AUTHORIZATION_SCHEMES.includes(state.firstWord.toLowerCase());
|
|
394
|
+
consumeRaw(1);
|
|
395
|
+
if (isScheme) {
|
|
396
|
+
state = { kind: 'authorization_value', phase: 'after_scheme', firstWord: '' };
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
output += character;
|
|
400
|
+
state = undefined;
|
|
401
|
+
}
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
const firstWord = `${state.firstWord}${character}`.slice(0, 7);
|
|
405
|
+
consumeRaw(1);
|
|
406
|
+
const couldBeScheme = AUTHORIZATION_SCHEMES.some((scheme) => scheme.startsWith(firstWord.toLowerCase()));
|
|
407
|
+
state = couldBeScheme
|
|
408
|
+
? { kind: 'authorization_value', phase: 'first_word', firstWord }
|
|
409
|
+
: { kind: 'authorization_value', phase: 'credential', firstWord: '' };
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (/\s/.test(character)) {
|
|
413
|
+
output += consumeRaw(1);
|
|
414
|
+
state = undefined;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
consumeRaw(1);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (state?.kind === 'approval_id_value') {
|
|
421
|
+
const character = pending[0] ?? '';
|
|
422
|
+
if (/[A-Za-z0-9]/.test(character)) {
|
|
423
|
+
consumeRaw(1);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
state = undefined;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (state?.kind === 'url_authority') {
|
|
430
|
+
const character = pending[0] ?? '';
|
|
431
|
+
if (/[\s/]/.test(character)) {
|
|
432
|
+
state = undefined;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
consumeRaw(1);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (state?.kind === 'timestamp_suffix') {
|
|
439
|
+
const character = pending[0] ?? '';
|
|
440
|
+
if (character === 'Z') {
|
|
441
|
+
consumeRaw(1);
|
|
442
|
+
state = undefined;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (character === '.') {
|
|
446
|
+
consumeRaw(1);
|
|
447
|
+
state = { kind: 'timestamp_fraction' };
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
state = undefined;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (state?.kind === 'timestamp_fraction') {
|
|
454
|
+
const character = pending[0] ?? '';
|
|
455
|
+
if (/\d/.test(character)) {
|
|
456
|
+
consumeRaw(1);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (character === 'Z')
|
|
460
|
+
consumeRaw(1);
|
|
461
|
+
state = undefined;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (state?.kind === 'high_entropy_value') {
|
|
465
|
+
const character = pending[0] ?? '';
|
|
466
|
+
if (/[A-Za-z0-9+/=]/.test(character)) {
|
|
467
|
+
consumeRaw(1);
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
state = undefined;
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
// Resolve the bounded high-entropy candidate before literal key names. Otherwise a token
|
|
474
|
+
// beginning with e.g. "token" could be mistaken for a failed key/value prefix and emitted.
|
|
475
|
+
const entropy = highEntropyAtStart();
|
|
476
|
+
if (entropy === 'awaiting' && !final)
|
|
477
|
+
break;
|
|
478
|
+
if (entropy === 'complete') {
|
|
479
|
+
consumeRaw(40);
|
|
480
|
+
output += '<high-entropy>';
|
|
481
|
+
state = { kind: 'high_entropy_value' };
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
const marker = markerAtStart();
|
|
485
|
+
if (marker === 'awaiting' && !final)
|
|
486
|
+
break;
|
|
487
|
+
if (marker && marker !== 'awaiting') {
|
|
488
|
+
const rawMarker = consumeRaw(marker.length);
|
|
489
|
+
switch (marker.kind) {
|
|
490
|
+
case 'quoted_authorization':
|
|
491
|
+
case 'quoted_generic_header':
|
|
492
|
+
output += `${rawMarker} <redacted>`;
|
|
493
|
+
state = { kind: 'quoted_value', quote: marker.quote ?? '"' };
|
|
494
|
+
break;
|
|
495
|
+
case 'authorization':
|
|
496
|
+
output += `${rawMarker} <redacted>`;
|
|
497
|
+
state = { kind: 'authorization_value', phase: 'leading', firstWord: '' };
|
|
498
|
+
break;
|
|
499
|
+
case 'generic_header':
|
|
500
|
+
output += `${rawMarker} <redacted>`;
|
|
501
|
+
state = { kind: 'non_whitespace_value', phase: 'leading' };
|
|
502
|
+
break;
|
|
503
|
+
case 'key_name':
|
|
504
|
+
output += rawMarker;
|
|
505
|
+
state = { kind: 'key_separator' };
|
|
506
|
+
break;
|
|
507
|
+
case 'bearer':
|
|
508
|
+
output += rawMarker;
|
|
509
|
+
state = { kind: 'bearer_wait' };
|
|
510
|
+
break;
|
|
511
|
+
case 'approval_command':
|
|
512
|
+
output += rawMarker;
|
|
513
|
+
state = { kind: 'approval_wait' };
|
|
514
|
+
break;
|
|
515
|
+
case 'approval_id':
|
|
516
|
+
output += '<approval-id>';
|
|
517
|
+
state = { kind: 'approval_id_value' };
|
|
518
|
+
break;
|
|
519
|
+
case 'mysql_password':
|
|
520
|
+
output += `${rawMarker}<redacted>`;
|
|
521
|
+
state = { kind: 'non_whitespace_value', phase: 'value' };
|
|
522
|
+
break;
|
|
523
|
+
case 'url_authority':
|
|
524
|
+
output += `${rawMarker}<redacted>`;
|
|
525
|
+
state = { kind: 'url_authority' };
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const uuid = uuidAtStart(final);
|
|
531
|
+
if (uuid === 'awaiting' && !final)
|
|
532
|
+
break;
|
|
533
|
+
if (uuid === 'complete') {
|
|
534
|
+
consumeRaw(UUID_SHAPE.length);
|
|
535
|
+
output += '<uuid>';
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
const timestamp = timestampAtStart();
|
|
539
|
+
if (timestamp === 'awaiting' && !final)
|
|
540
|
+
break;
|
|
541
|
+
if (timestamp === 'complete') {
|
|
542
|
+
consumeRaw(TIMESTAMP_SHAPE.length);
|
|
543
|
+
output += '<timestamp>';
|
|
544
|
+
state = { kind: 'timestamp_suffix' };
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
output += consumeRaw(1);
|
|
548
|
+
}
|
|
549
|
+
return output;
|
|
550
|
+
};
|
|
551
|
+
return {
|
|
552
|
+
get bufferedBytes() {
|
|
553
|
+
return Buffer.byteLength(pending);
|
|
554
|
+
},
|
|
555
|
+
write(chunk) {
|
|
556
|
+
const bytes = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
|
|
557
|
+
pending += decoder.write(bytes);
|
|
558
|
+
return drain(false);
|
|
559
|
+
},
|
|
560
|
+
end() {
|
|
561
|
+
pending += decoder.end();
|
|
562
|
+
return drain(true);
|
|
563
|
+
},
|
|
564
|
+
};
|
|
565
|
+
}
|
|
72
566
|
export function scrubValue(value, options = DEFAULT_SCRUB_OPTIONS) {
|
|
73
567
|
if (typeof value === 'string') {
|
|
74
568
|
return scrubString(value, options);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { BelayConfigV4 } from './config.js';
|
|
2
2
|
import type { GatedActionKind } from './gate-contract.js';
|
|
3
|
-
import type { ClassifyResult } from './types.js';
|
|
4
3
|
export type StandingAllowSource = 'operator' | 'availability-reconfirmed';
|
|
5
4
|
export interface StandingAllowEntry {
|
|
6
5
|
kind: GatedActionKind;
|
|
@@ -16,29 +15,10 @@ export interface StandingAllowFile {
|
|
|
16
15
|
version: 1;
|
|
17
16
|
entries: StandingAllowEntry[];
|
|
18
17
|
}
|
|
19
|
-
export interface StandingAllowMatch {
|
|
20
|
-
source: StandingAllowSource;
|
|
21
|
-
entryId?: string;
|
|
22
|
-
}
|
|
23
|
-
/** Default TTL for operator / availability-reconfirmed standing-allow entries. */
|
|
24
|
-
export declare const DEFAULT_STANDING_ALLOW_TTL_MS: number;
|
|
25
18
|
export declare function standingAllowFile(config: BelayConfigV4, repoLocalStateDir: string): string;
|
|
26
|
-
export declare function isTier0StandingAllowBlocked(result: ClassifyResult): boolean;
|
|
27
19
|
export declare function compactStandingAllow(state: StandingAllowFile, now?: number): StandingAllowFile;
|
|
28
20
|
export declare function loadStandingAllow(filePath: string): Promise<StandingAllowFile>;
|
|
29
21
|
export declare function saveStandingAllow(filePath: string, state: StandingAllowFile): Promise<void>;
|
|
30
|
-
export declare function resolveStandingAllowMatch(params: {
|
|
31
|
-
kind: GatedActionKind;
|
|
32
|
-
result: ClassifyResult;
|
|
33
|
-
repoRoot: string;
|
|
34
|
-
state: StandingAllowFile;
|
|
35
|
-
now?: number;
|
|
36
|
-
}): StandingAllowMatch | null;
|
|
37
|
-
export declare function addStandingAllowEntry(state: StandingAllowFile, entry: Omit<StandingAllowEntry, 'createdAt' | 'expiresAt'> & {
|
|
38
|
-
createdAt?: string;
|
|
39
|
-
expiresAt?: string;
|
|
40
|
-
ttlMs?: number;
|
|
41
|
-
}): StandingAllowFile;
|
|
42
22
|
export declare function revokeStandingAllowEntry(state: StandingAllowFile, params: {
|
|
43
23
|
kind: GatedActionKind;
|
|
44
24
|
fingerprint: string;
|
|
@@ -6,29 +6,9 @@ const EMPTY_STANDING_ALLOW = {
|
|
|
6
6
|
version: 1,
|
|
7
7
|
entries: [],
|
|
8
8
|
};
|
|
9
|
-
/** Default TTL for operator / availability-reconfirmed standing-allow entries. */
|
|
10
|
-
export const DEFAULT_STANDING_ALLOW_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
11
|
-
/** Reasons that must never be silenced via standing-allow (defense in depth with signal checks). */
|
|
12
|
-
const STANDING_ALLOW_BLOCKED_REASONS = new Set([
|
|
13
|
-
'external_effect',
|
|
14
|
-
'tier1_catastrophic',
|
|
15
|
-
'protected_artifact',
|
|
16
|
-
'pipe_to_shell',
|
|
17
|
-
'command_substitution',
|
|
18
|
-
'outside_repo_mutation',
|
|
19
|
-
'outside_repo_redirect',
|
|
20
|
-
'control_plane_mutation',
|
|
21
|
-
]);
|
|
22
9
|
export function standingAllowFile(config, repoLocalStateDir) {
|
|
23
10
|
return `${belayStateDir(config, repoLocalStateDir)}/standing-allow.json`;
|
|
24
11
|
}
|
|
25
|
-
export function isTier0StandingAllowBlocked(result) {
|
|
26
|
-
if (result.reason.startsWith('tier0_') || STANDING_ALLOW_BLOCKED_REASONS.has(result.reason)) {
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
const signals = result.assessment?.signals ?? [];
|
|
30
|
-
return signals.some((signal) => signal === 'tier0_external' || signal === 'tier1_catastrophic');
|
|
31
|
-
}
|
|
32
12
|
function isExpired(iso, now = Date.now()) {
|
|
33
13
|
const parsed = Date.parse(iso);
|
|
34
14
|
return Number.isNaN(parsed) || parsed <= now;
|
|
@@ -92,57 +72,6 @@ export async function saveStandingAllow(filePath, state) {
|
|
|
92
72
|
const compacted = compactStandingAllow(state);
|
|
93
73
|
await writeFile(filePath, `${JSON.stringify(compacted, null, 2)}\n`, 'utf8');
|
|
94
74
|
}
|
|
95
|
-
function matchStateEntry(params) {
|
|
96
|
-
const now = params.now ?? Date.now();
|
|
97
|
-
const match = params.state.entries.find((entry) => entry.kind === params.kind &&
|
|
98
|
-
entry.fingerprint === params.fingerprint &&
|
|
99
|
-
!isExpired(entry.expiresAt, now) &&
|
|
100
|
-
(entry.repoRoot === undefined || entry.repoRoot === params.repoRoot));
|
|
101
|
-
if (!match) {
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
return { source: match.source, entryId: match.fingerprint };
|
|
105
|
-
}
|
|
106
|
-
export function resolveStandingAllowMatch(params) {
|
|
107
|
-
if (params.kind === 'shell') {
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
if (params.result.verdict !== 'deny_pending_approval') {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
if (isTier0StandingAllowBlocked(params.result)) {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
return matchStateEntry({
|
|
117
|
-
kind: params.kind,
|
|
118
|
-
fingerprint: params.result.fingerprint,
|
|
119
|
-
repoRoot: params.repoRoot,
|
|
120
|
-
state: params.state,
|
|
121
|
-
now: params.now,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
export function addStandingAllowEntry(state, entry) {
|
|
125
|
-
const createdAt = entry.createdAt ?? new Date().toISOString();
|
|
126
|
-
const ttlMs = entry.ttlMs ?? DEFAULT_STANDING_ALLOW_TTL_MS;
|
|
127
|
-
const expiresAt = entry.expiresAt ?? new Date(Date.parse(createdAt) + ttlMs).toISOString();
|
|
128
|
-
const next = {
|
|
129
|
-
kind: entry.kind,
|
|
130
|
-
fingerprint: entry.fingerprint,
|
|
131
|
-
source: entry.source,
|
|
132
|
-
reason: entry.reason,
|
|
133
|
-
createdAt,
|
|
134
|
-
expiresAt,
|
|
135
|
-
...(entry.summary ? { summary: entry.summary } : {}),
|
|
136
|
-
...(entry.repoRoot ? { repoRoot: entry.repoRoot } : {}),
|
|
137
|
-
};
|
|
138
|
-
const filtered = state.entries.filter((existing) => !(existing.kind === next.kind &&
|
|
139
|
-
existing.fingerprint === next.fingerprint &&
|
|
140
|
-
existing.repoRoot === next.repoRoot));
|
|
141
|
-
return compactStandingAllow({
|
|
142
|
-
version: 1,
|
|
143
|
-
entries: [...filtered, next],
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
75
|
export function revokeStandingAllowEntry(state, params) {
|
|
147
76
|
const before = state.entries.length;
|
|
148
77
|
const entries = state.entries.filter((entry) => {
|
|
@@ -7,6 +7,7 @@ export interface ApplyObservedChangesParams {
|
|
|
7
7
|
sourceRoot: string;
|
|
8
8
|
targetRoot: string;
|
|
9
9
|
changes: ObservedFileChange[];
|
|
10
|
+
beforeMutation?: () => Promise<void>;
|
|
10
11
|
afterApply?: () => Promise<void>;
|
|
11
12
|
}
|
|
12
13
|
export declare function applyObservedChanges(params: ApplyObservedChangesParams): Promise<void>;
|
|
@@ -201,6 +201,7 @@ export async function applyObservedChanges(params) {
|
|
|
201
201
|
const backupRoot = await mkdtemp(path.join(os.tmpdir(), 'belay-tx-rollback-'));
|
|
202
202
|
const rollbackActions = [];
|
|
203
203
|
let mutationAttempted = false;
|
|
204
|
+
let resourceIdentityChanged = false;
|
|
204
205
|
try {
|
|
205
206
|
for (const change of sortedChanges(changes)) {
|
|
206
207
|
const target = joinRelativePath(targetRoot, change.relativePath);
|
|
@@ -232,6 +233,13 @@ export async function applyObservedChanges(params) {
|
|
|
232
233
|
await assertParentChainSafe(targetRoot, change.relativePath);
|
|
233
234
|
rollbackActions.push({ type: 'remove', target, relativePath: change.relativePath });
|
|
234
235
|
}
|
|
236
|
+
try {
|
|
237
|
+
await params.beforeMutation?.();
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
resourceIdentityChanged = true;
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
235
243
|
mutationAttempted = true;
|
|
236
244
|
await applySingleChange(sourceRoot, targetRoot, change);
|
|
237
245
|
}
|
|
@@ -248,6 +256,9 @@ export async function applyObservedChanges(params) {
|
|
|
248
256
|
catch (error) {
|
|
249
257
|
if (!mutationAttempted)
|
|
250
258
|
throw error;
|
|
259
|
+
if (resourceIdentityChanged) {
|
|
260
|
+
throw new Error(TRANSACTIONAL_APPLY_ROLLBACK_FAILED, { cause: error });
|
|
261
|
+
}
|
|
251
262
|
const rollbackOk = await rollbackAppliedChanges(rollbackActions, changes, targetRoot, backupRoot);
|
|
252
263
|
if (!rollbackOk) {
|
|
253
264
|
throw new Error(TRANSACTIONAL_APPLY_ROLLBACK_FAILED);
|