vouchington-tooling 0.45.0 → 0.47.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 +10 -52
- package/dist/agent-blackboard/feedback-audit-source.d.mts +4 -10
- package/dist/agent-blackboard/feedback-audit-source.mjs +7 -18
- package/dist/agent-blackboard/feedback-compose-audit.mjs +6 -14
- package/dist/agent-blackboard/feedback-compose.d.mts +1 -4
- package/dist/agent-blackboard/feedback-compose.mjs +2 -5
- package/dist/agent-blackboard/feedback-fields.mjs +1 -1
- package/dist/agent-blackboard/feedback-identity.mjs +1 -1
- package/dist/agent-blackboard/feedback-journal-audit.d.mts +6 -8
- package/dist/agent-blackboard/feedback-journal-audit.mjs +9 -10
- package/dist/agent-blackboard/feedback-journal-sandbox.d.mts +2 -2
- package/dist/agent-blackboard/feedback-journal-sandbox.mjs +2 -2
- package/dist/agent-blackboard/feedback-outbox-rejected.mjs +1 -1
- package/dist/agent-blackboard/feedback-outbox.mjs +2 -2
- package/dist/agent-blackboard/index.d.mts +1 -1
- package/dist/audit-journal/ci-failures.d.mts +6 -0
- package/dist/{session-friction → audit-journal}/ci-failures.mjs +3 -9
- package/dist/{session-friction → audit-journal}/journal.d.mts +0 -1
- package/dist/{session-friction → audit-journal}/journal.mjs +1 -1
- package/dist/audit-journal/session-id.d.mts +1 -0
- package/dist/{session-friction → audit-journal}/session-id.mjs +0 -5
- package/dist/{session-friction → audit-journal}/text.d.mts +1 -4
- package/dist/{session-friction → audit-journal}/text.mjs +4 -22
- package/dist/audit-journal/types.d.mts +17 -0
- package/dist/cli/commands/gha-runtime-audit.mjs +2 -0
- package/dist/gh-cli/env.d.mts +9 -0
- package/dist/gh-cli/env.mjs +12 -0
- package/dist/gh-cli/exec.d.mts +7 -4
- package/dist/gh-cli/exec.mjs +7 -5
- package/dist/gh-cli/index.d.mts +1 -0
- package/dist/gh-cli/index.mjs +1 -0
- package/dist/gha-post-review/github.mjs +2 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +2 -2
- package/dist/mcp-server/context.d.mts +2 -0
- package/dist/mcp-server/dispatch.d.mts +2 -0
- package/dist/mcp-server/dispatch.mjs +1 -0
- package/dist/mcp-server/harness.test-helpers.d.mts +1 -0
- package/dist/mcp-server/harness.test-helpers.mjs +1 -0
- package/dist/mcp-server/pr-agent.d.mts +12 -0
- package/dist/mcp-server/pr-agent.mjs +34 -0
- package/dist/mcp-server/pr-body.d.mts +5 -3
- package/dist/mcp-server/pr-body.mjs +12 -13
- package/dist/mcp-server/pr-gh.mjs +2 -1
- package/dist/mcp-server/pr-input.d.mts +5 -2
- package/dist/mcp-server/pr-input.mjs +22 -4
- package/dist/mcp-server/pr-live.d.mts +1 -0
- package/dist/mcp-server/pr-live.mjs +1 -1
- package/dist/mcp-server/pr-prepare.mjs +25 -4
- package/dist/mcp-server/server.mjs +1 -0
- package/dist/mcp-server/tools-pr.mjs +20 -17
- package/dist/{session-friction → private-files}/lock.mjs +3 -2
- package/dist/private-files/log-file.d.mts +3 -0
- package/dist/private-files/log-file.mjs +53 -0
- package/dist/{session-friction → private-files}/utf8.mjs +1 -1
- package/dist/retrospective-facts/exec.mjs +2 -1
- package/docs/mcp-server.md +29 -10
- package/package.json +1 -6
- package/dist/session-friction/ci-failures.d.mts +0 -13
- package/dist/session-friction/classify.d.mts +0 -2
- package/dist/session-friction/classify.mjs +0 -98
- package/dist/session-friction/command-segments.d.mts +0 -3
- package/dist/session-friction/command-segments.mjs +0 -94
- package/dist/session-friction/coverage-ledger.d.mts +0 -10
- package/dist/session-friction/coverage-ledger.mjs +0 -75
- package/dist/session-friction/directory.d.mts +0 -1
- package/dist/session-friction/directory.mjs +0 -71
- package/dist/session-friction/index.d.mts +0 -6
- package/dist/session-friction/index.mjs +0 -5
- package/dist/session-friction/log-file.d.mts +0 -7
- package/dist/session-friction/log-file.mjs +0 -95
- package/dist/session-friction/log.d.mts +0 -8
- package/dist/session-friction/log.mjs +0 -163
- package/dist/session-friction/normalize.d.mts +0 -1
- package/dist/session-friction/normalize.mjs +0 -81
- package/dist/session-friction/report.d.mts +0 -16
- package/dist/session-friction/report.mjs +0 -91
- package/dist/session-friction/sandbox.d.mts +0 -3
- package/dist/session-friction/sandbox.mjs +0 -43
- package/dist/session-friction/session-id.d.mts +0 -2
- package/dist/session-friction/types.d.mts +0 -80
- /package/dist/{session-friction → audit-journal}/audit-block.d.mts +0 -0
- /package/dist/{session-friction → audit-journal}/audit-block.mjs +0 -0
- /package/dist/{session-friction → audit-journal}/audit-fit.d.mts +0 -0
- /package/dist/{session-friction → audit-journal}/audit-fit.mjs +0 -0
- /package/dist/{session-friction → audit-journal}/types.mjs +0 -0
- /package/dist/{session-friction → private-files}/lock.d.mts +0 -0
- /package/dist/{session-friction → private-files}/utf8.d.mts +0 -0
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { closeSync, constants, fstatSync, openSync, readSync, writeSync } from 'node:fs';
|
|
2
|
-
import { isSafeAuditText } from "./text.mjs";
|
|
3
|
-
import { decodeUtf8 } from "./utf8.mjs";
|
|
4
|
-
export const LOG_MAX_BYTES = 2_000_000;
|
|
5
|
-
export function validEvent(value) {
|
|
6
|
-
if (!value || typeof value !== 'object')
|
|
7
|
-
return false;
|
|
8
|
-
// oxlint-disable-next-line no-mistakes/ts-no-const-aliases -- establish a record view after the runtime object check
|
|
9
|
-
const record = value;
|
|
10
|
-
return ((record.kind === 'sandbox-escalation' ||
|
|
11
|
-
record.kind === 'sandbox-failure' ||
|
|
12
|
-
record.kind === 'ambiguous-failure') &&
|
|
13
|
-
isSafeAuditText(record.timestamp) &&
|
|
14
|
-
isSafeAuditText(record.commandPrefix) &&
|
|
15
|
-
isSafeAuditText(record.detail) &&
|
|
16
|
-
(record.failure === undefined || validFailure(record.failure)) &&
|
|
17
|
-
(record.outcome === undefined ||
|
|
18
|
-
(typeof record.outcome === 'string' &&
|
|
19
|
-
['requested', 'approved', 'denied', 'unknown'].includes(record.outcome))));
|
|
20
|
-
}
|
|
21
|
-
function validFailure(value) {
|
|
22
|
-
if (!value || typeof value !== 'object')
|
|
23
|
-
return false;
|
|
24
|
-
// oxlint-disable-next-line no-mistakes/ts-no-const-aliases -- establish a validated object view
|
|
25
|
-
const failure = value;
|
|
26
|
-
return (Object.keys(failure).length === 2 &&
|
|
27
|
-
(failure.kind === 'sandbox-failure' || failure.kind === 'ambiguous-failure') &&
|
|
28
|
-
isSafeAuditText(failure.detail));
|
|
29
|
-
}
|
|
30
|
-
export function openLogFile(path, flags) {
|
|
31
|
-
let descriptor;
|
|
32
|
-
try {
|
|
33
|
-
descriptor = openSync(path, flags | constants.O_NOFOLLOW | constants.O_NONBLOCK, 0o600);
|
|
34
|
-
}
|
|
35
|
-
catch (error) {
|
|
36
|
-
if (error &&
|
|
37
|
-
typeof error === 'object' &&
|
|
38
|
-
'code' in error &&
|
|
39
|
-
(error.code === 'ELOOP' || error.code === 'EISDIR' || error.code === 'ENOTDIR'))
|
|
40
|
-
throw new Error('session-friction log must be a regular file');
|
|
41
|
-
throw error;
|
|
42
|
-
}
|
|
43
|
-
try {
|
|
44
|
-
const status = fstatSync(descriptor);
|
|
45
|
-
if (!status.isFile() || status.nlink !== 1)
|
|
46
|
-
throw new Error('session-friction log must be a regular file');
|
|
47
|
-
return descriptor;
|
|
48
|
-
}
|
|
49
|
-
catch (error) {
|
|
50
|
-
closeSync(descriptor);
|
|
51
|
-
throw error;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
export function readLogContent(descriptor, maximumBytes = LOG_MAX_BYTES) {
|
|
55
|
-
if (fstatSync(descriptor).size > maximumBytes)
|
|
56
|
-
throw new Error('session-friction log is too large');
|
|
57
|
-
const buffer = Buffer.alloc(maximumBytes + 1);
|
|
58
|
-
let length = 0;
|
|
59
|
-
while (length < buffer.length) {
|
|
60
|
-
const bytes = readSync(descriptor, buffer, length, buffer.length - length, length);
|
|
61
|
-
if (bytes === 0)
|
|
62
|
-
break;
|
|
63
|
-
length += bytes;
|
|
64
|
-
}
|
|
65
|
-
/* v8 ignore next -- detects external growth after the descriptor size check. */
|
|
66
|
-
if (length > maximumBytes)
|
|
67
|
-
throw new Error('session-friction log is too large');
|
|
68
|
-
return decodeUtf8(buffer.subarray(0, length));
|
|
69
|
-
}
|
|
70
|
-
export function validEventCount(content, limit) {
|
|
71
|
-
let count = 0;
|
|
72
|
-
for (const line of content.split('\n')) {
|
|
73
|
-
if (!line.trim())
|
|
74
|
-
continue;
|
|
75
|
-
try {
|
|
76
|
-
if (validEvent(JSON.parse(line)))
|
|
77
|
-
count++;
|
|
78
|
-
}
|
|
79
|
-
catch { }
|
|
80
|
-
if (count >= limit)
|
|
81
|
-
break;
|
|
82
|
-
}
|
|
83
|
-
return count;
|
|
84
|
-
}
|
|
85
|
-
export function writeAll(descriptor, value) {
|
|
86
|
-
const buffer = Buffer.from(value);
|
|
87
|
-
let offset = 0;
|
|
88
|
-
while (offset < buffer.length) {
|
|
89
|
-
const written = writeSync(descriptor, buffer, offset, buffer.length - offset);
|
|
90
|
-
/* v8 ignore next -- a zero-byte synchronous file write is an external I/O failure. */
|
|
91
|
-
if (written === 0)
|
|
92
|
-
throw new Error('session-friction log write made no progress');
|
|
93
|
-
offset += written;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { FrictionLogOptions, FrictionLogReadResult, FrictionObservation } from './types.mts';
|
|
2
|
-
export declare const FRICTION_LOG_MAX_EVENTS = 500;
|
|
3
|
-
export declare function requireDirectory(directory: string): string;
|
|
4
|
-
export declare function eventLimit(maxEvents: number | undefined): number;
|
|
5
|
-
export declare function recordFriction(sessionId: string, observation: FrictionObservation, options: FrictionLogOptions & {
|
|
6
|
-
timestamp?: string | (() => string);
|
|
7
|
-
}): void;
|
|
8
|
-
export declare function readFrictionLog(sessionId: string, options: FrictionLogOptions): FrictionLogReadResult;
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { readCoverageLedger, writeCoverageLedger } from "./coverage-ledger.mjs";
|
|
2
|
-
import { closeSync, constants, fchmodSync, fstatSync } from 'node:fs';
|
|
3
|
-
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
-
import { classifyFrictionObservation } from "./classify.mjs";
|
|
5
|
-
import { ensurePrivateDirectory } from "./directory.mjs";
|
|
6
|
-
import { withFileLock } from "./lock.mjs";
|
|
7
|
-
import { validEventCount, LOG_MAX_BYTES, openLogFile, readLogContent, validEvent, writeAll, } from "./log-file.mjs";
|
|
8
|
-
import { sanitizeSessionId } from "./session-id.mjs";
|
|
9
|
-
import { boundedText, isWellFormedUnicode, normalizeAuditText } from "./text.mjs";
|
|
10
|
-
export const FRICTION_LOG_MAX_EVENTS = 500;
|
|
11
|
-
const EVENT_FIELD_MAX_LENGTH = 1_000;
|
|
12
|
-
export function requireDirectory(directory) {
|
|
13
|
-
if (typeof directory !== 'string')
|
|
14
|
-
throw new Error('log directory must be a string');
|
|
15
|
-
if (Buffer.byteLength(directory) > 4096)
|
|
16
|
-
throw new Error('session-friction log directory path is too long');
|
|
17
|
-
if (!isAbsolute(directory))
|
|
18
|
-
throw new Error('session-friction log directory must be absolute');
|
|
19
|
-
return resolve(directory);
|
|
20
|
-
}
|
|
21
|
-
export function eventLimit(maxEvents) {
|
|
22
|
-
const limit = maxEvents ?? FRICTION_LOG_MAX_EVENTS;
|
|
23
|
-
if (!Number.isInteger(limit) || limit < 1)
|
|
24
|
-
throw new Error('maxEvents must be a positive integer');
|
|
25
|
-
return Math.min(limit, FRICTION_LOG_MAX_EVENTS);
|
|
26
|
-
}
|
|
27
|
-
function logPath(sessionId, directory) {
|
|
28
|
-
return join(requireDirectory(directory), `${sanitizeSessionId(sessionId)}.jsonl`);
|
|
29
|
-
}
|
|
30
|
-
export function recordFriction(sessionId, observation, options) {
|
|
31
|
-
const maxEvents = eventLimit(options.maxEvents);
|
|
32
|
-
const timestamp = typeof options.timestamp === 'function' ? options.timestamp() : options.timestamp;
|
|
33
|
-
if (timestamp !== undefined && typeof timestamp !== 'string')
|
|
34
|
-
throw new Error('timestamp must be a string');
|
|
35
|
-
const rawTimestamp = timestamp ?? new Date().toISOString();
|
|
36
|
-
const normalizedTimestamp = normalizeAuditText(boundedText(rawTimestamp, EVENT_FIELD_MAX_LENGTH));
|
|
37
|
-
if (!normalizedTimestamp)
|
|
38
|
-
throw new Error('timestamp must be non-empty');
|
|
39
|
-
if (!isWellFormedUnicode(normalizedTimestamp))
|
|
40
|
-
throw new Error('timestamp must be well-formed Unicode');
|
|
41
|
-
const classified = classifyFrictionObservation(observation);
|
|
42
|
-
const path = logPath(sessionId, options.directory);
|
|
43
|
-
const directory = requireDirectory(options.directory);
|
|
44
|
-
ensurePrivateDirectory(directory, true);
|
|
45
|
-
withFileLock(path, () => {
|
|
46
|
-
/* v8 ignore next 2 -- requires the directory path to be replaced after its lock is acquired. */
|
|
47
|
-
if (!ensurePrivateDirectory(directory, false))
|
|
48
|
-
throw new Error('session-friction log directory disappeared while acquiring the lock');
|
|
49
|
-
const descriptor = openLogFile(path, constants.O_CREAT | constants.O_RDWR | constants.O_APPEND);
|
|
50
|
-
try {
|
|
51
|
-
fchmodSync(descriptor, 0o600);
|
|
52
|
-
if (fstatSync(descriptor).size > LOG_MAX_BYTES)
|
|
53
|
-
throw new Error('session-friction log is too large');
|
|
54
|
-
if (!classified)
|
|
55
|
-
return;
|
|
56
|
-
const coverage = readCoverageLedger(path);
|
|
57
|
-
const logBytes = fstatSync(descriptor).size;
|
|
58
|
-
if (coverage && coverage.logBytes === logBytes && coverage.retainedEvents >= maxEvents) {
|
|
59
|
-
writeCoverageLedger(path, { ...coverage, droppedCount: coverage.droppedCount + 1 });
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
const content = readLogContent(descriptor);
|
|
63
|
-
const retainedEvents = validEventCount(content, FRICTION_LOG_MAX_EVENTS);
|
|
64
|
-
if (retainedEvents >= maxEvents) {
|
|
65
|
-
writeCoverageLedger(path, {
|
|
66
|
-
schemaVersion: 1,
|
|
67
|
-
logBytes,
|
|
68
|
-
retainedEvents,
|
|
69
|
-
droppedCount: (coverage?.droppedCount ?? 0) + 1,
|
|
70
|
-
});
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
const event = {
|
|
74
|
-
...classified,
|
|
75
|
-
commandPrefix: normalizeAuditText(classified.commandPrefix),
|
|
76
|
-
detail: normalizeAuditText(classified.detail),
|
|
77
|
-
timestamp: normalizedTimestamp,
|
|
78
|
-
};
|
|
79
|
-
/* v8 ignore next -- normalized classified fields satisfy validEvent defensively. */
|
|
80
|
-
if (!validEvent(event))
|
|
81
|
-
return;
|
|
82
|
-
const prefix = content && !content.endsWith('\n') ? '\n' : '';
|
|
83
|
-
const addition = `${prefix}${JSON.stringify(event)}\n`;
|
|
84
|
-
if (logBytes + Buffer.byteLength(addition) > LOG_MAX_BYTES) {
|
|
85
|
-
writeCoverageLedger(path, {
|
|
86
|
-
schemaVersion: 1,
|
|
87
|
-
logBytes,
|
|
88
|
-
retainedEvents,
|
|
89
|
-
droppedCount: (coverage?.droppedCount ?? 0) + 1,
|
|
90
|
-
});
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
writeAll(descriptor, addition);
|
|
94
|
-
if (coverage)
|
|
95
|
-
writeCoverageLedger(path, {
|
|
96
|
-
...coverage,
|
|
97
|
-
logBytes: fstatSync(descriptor).size,
|
|
98
|
-
retainedEvents: retainedEvents + 1,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
finally {
|
|
102
|
-
closeSync(descriptor);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
export function readFrictionLog(sessionId, options) {
|
|
107
|
-
const maxEvents = eventLimit(options.maxEvents);
|
|
108
|
-
const path = logPath(sessionId, options.directory);
|
|
109
|
-
const directory = requireDirectory(options.directory);
|
|
110
|
-
if (!ensurePrivateDirectory(directory, false))
|
|
111
|
-
return { status: 'absent' };
|
|
112
|
-
try {
|
|
113
|
-
return withFileLock(path, () => {
|
|
114
|
-
/* v8 ignore next -- detects directory removal after acquiring the file lock. */
|
|
115
|
-
if (!ensurePrivateDirectory(directory, false))
|
|
116
|
-
return { status: 'absent' };
|
|
117
|
-
const descriptor = openLogFile(path, constants.O_RDONLY);
|
|
118
|
-
try {
|
|
119
|
-
const events = [];
|
|
120
|
-
const ledger = readCoverageLedger(path);
|
|
121
|
-
let droppedCount = ledger?.droppedCount ?? 0;
|
|
122
|
-
let retainedEvents = 0;
|
|
123
|
-
for (const line of readLogContent(descriptor).split('\n')) {
|
|
124
|
-
if (!line.trim())
|
|
125
|
-
continue;
|
|
126
|
-
try {
|
|
127
|
-
const value = JSON.parse(line);
|
|
128
|
-
if (validEvent(value)) {
|
|
129
|
-
retainedEvents++;
|
|
130
|
-
if (events.length < maxEvents)
|
|
131
|
-
events.push(value);
|
|
132
|
-
else
|
|
133
|
-
droppedCount++;
|
|
134
|
-
}
|
|
135
|
-
else
|
|
136
|
-
droppedCount++;
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
droppedCount++;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (ledger &&
|
|
143
|
-
(ledger.logBytes !== fstatSync(descriptor).size ||
|
|
144
|
-
ledger.retainedEvents !== Math.min(retainedEvents, FRICTION_LOG_MAX_EVENTS)))
|
|
145
|
-
throw new Error('session-friction coverage ledger does not match retained log');
|
|
146
|
-
if (!Number.isSafeInteger(droppedCount))
|
|
147
|
-
throw new Error('session-friction dropped count overflow; capture incomplete');
|
|
148
|
-
const coverage = droppedCount ? { truncated: true, droppedCount } : {};
|
|
149
|
-
return events.length
|
|
150
|
-
? { status: 'events', events, ...coverage }
|
|
151
|
-
: { status: 'empty', ...coverage };
|
|
152
|
-
}
|
|
153
|
-
finally {
|
|
154
|
-
closeSync(descriptor);
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
catch (error) {
|
|
159
|
-
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT')
|
|
160
|
-
return { status: 'absent' };
|
|
161
|
-
throw error;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function normalizeCommandPrefix(command: string, wrappersToStrip?: string[]): string;
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { REDACTED_TOKEN, splitCommand, stripAssignments } from "./command-segments.mjs";
|
|
2
|
-
const MAX_COMMAND_TOKEN_LENGTH = 40;
|
|
3
|
-
const MAX_COMMAND_LENGTH = 100_000;
|
|
4
|
-
const MAX_COMMAND_WRAPPERS = 16;
|
|
5
|
-
const CREDENTIAL_OPTION = /^--?(?:api[-_]?key|auth|credential|password|secret|token)=/i;
|
|
6
|
-
const URL_USERINFO = /^[a-z][a-z0-9+.-]*:\/\/[^/@\s]+@/i;
|
|
7
|
-
const PACKAGE_RUNNERS = new Set(['npm', 'npx', 'pnpm', 'pnpx', 'yarn']);
|
|
8
|
-
const GIT_OPTIONS_WITH_ARGS = new Set(['-C', '-c', '--git-dir', '--work-tree']);
|
|
9
|
-
const GIT_OPTIONS_WITHOUT_ARGS = new Set([
|
|
10
|
-
'-v',
|
|
11
|
-
'--version',
|
|
12
|
-
'-h',
|
|
13
|
-
'--help',
|
|
14
|
-
'--exec-path',
|
|
15
|
-
'--html-path',
|
|
16
|
-
'--man-path',
|
|
17
|
-
'--info-path',
|
|
18
|
-
'-p',
|
|
19
|
-
'--paginate',
|
|
20
|
-
'-P',
|
|
21
|
-
'--no-pager',
|
|
22
|
-
'--no-replace-objects',
|
|
23
|
-
'--bare',
|
|
24
|
-
]);
|
|
25
|
-
function redact(token) {
|
|
26
|
-
return token.length > MAX_COMMAND_TOKEN_LENGTH ||
|
|
27
|
-
CREDENTIAL_OPTION.test(token) ||
|
|
28
|
-
URL_USERINFO.test(token)
|
|
29
|
-
? REDACTED_TOKEN
|
|
30
|
-
: token;
|
|
31
|
-
}
|
|
32
|
-
function stripGitOptions(tokens) {
|
|
33
|
-
let index = 0;
|
|
34
|
-
while (index < tokens.length) {
|
|
35
|
-
const token = tokens[index];
|
|
36
|
-
if (token === '--')
|
|
37
|
-
return tokens.slice(index + 1);
|
|
38
|
-
if (GIT_OPTIONS_WITH_ARGS.has(token)) {
|
|
39
|
-
if (index + 1 >= tokens.length)
|
|
40
|
-
return [REDACTED_TOKEN];
|
|
41
|
-
index += 2;
|
|
42
|
-
}
|
|
43
|
-
else if (GIT_OPTIONS_WITHOUT_ARGS.has(token) || /^--[a-z0-9-]+=/i.test(token))
|
|
44
|
-
index++;
|
|
45
|
-
else
|
|
46
|
-
break;
|
|
47
|
-
}
|
|
48
|
-
return tokens.slice(index);
|
|
49
|
-
}
|
|
50
|
-
function packageRunner(token) {
|
|
51
|
-
return [...PACKAGE_RUNNERS].some((runner) => token === runner || token.endsWith(`/${runner}`));
|
|
52
|
-
}
|
|
53
|
-
function normalizeSegment(tokens) {
|
|
54
|
-
const [first, ...rest] = tokens;
|
|
55
|
-
if (!first)
|
|
56
|
-
return '';
|
|
57
|
-
if (packageRunner(first) && (rest[0] === 'run' || rest[0] === 'exec') && rest[1])
|
|
58
|
-
return `${redact(first)} ${rest[0]} ${redact(rest[1])}`;
|
|
59
|
-
const effective = first === 'git' || first.endsWith('/git') ? stripGitOptions(rest) : rest;
|
|
60
|
-
return effective[0] ? `${redact(first)} ${redact(effective[0])}` : redact(first);
|
|
61
|
-
}
|
|
62
|
-
export function normalizeCommandPrefix(command, wrappersToStrip = []) {
|
|
63
|
-
if (command.length > MAX_COMMAND_LENGTH)
|
|
64
|
-
return '[REDACTED: command too long]';
|
|
65
|
-
const segments = splitCommand(command);
|
|
66
|
-
let index = 0;
|
|
67
|
-
while (index < segments.length - 1 && stripAssignments(segments[index])[0] === 'cd')
|
|
68
|
-
index++;
|
|
69
|
-
const tokens = stripAssignments(segments[index] ?? []);
|
|
70
|
-
const wrapperValues = Array.isArray(wrappersToStrip) ? wrappersToStrip : [];
|
|
71
|
-
const wrapperNames = new Set(wrapperValues
|
|
72
|
-
.slice(0, MAX_COMMAND_WRAPPERS)
|
|
73
|
-
.filter((value) => typeof value === 'string' && value.length > 0 && value.length <= MAX_COMMAND_TOKEN_LENGTH));
|
|
74
|
-
let wrappers = 0;
|
|
75
|
-
while (tokens[wrappers] && wrapperNames.has(tokens[wrappers]))
|
|
76
|
-
wrappers++;
|
|
77
|
-
if (!wrappers)
|
|
78
|
-
return normalizeSegment(tokens);
|
|
79
|
-
const nested = normalizeSegment(stripAssignments(tokens.slice(wrappers)));
|
|
80
|
-
return nested ? `${redact(tokens[0])} ${nested}` : redact(tokens[0]);
|
|
81
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { type JournalScan } from './journal.mts';
|
|
2
|
-
import type { FrictionLogReadResult, SessionFrictionReport, SessionFrictionReportOptions } from './types.mts';
|
|
3
|
-
type FrictionRead = {
|
|
4
|
-
result: FrictionLogReadResult;
|
|
5
|
-
} | {
|
|
6
|
-
error: unknown;
|
|
7
|
-
};
|
|
8
|
-
/** Everything a report reads from outside, loaded once so rendering can be repeated for free. */
|
|
9
|
-
export type FrictionReportInputs = {
|
|
10
|
-
scanned: JournalScan;
|
|
11
|
-
read: FrictionRead;
|
|
12
|
-
};
|
|
13
|
-
export declare function loadFrictionReportInputs(sessionId: string, options: SessionFrictionReportOptions): Promise<FrictionReportInputs>;
|
|
14
|
-
export declare function renderFrictionReport(sessionId: string, options: SessionFrictionReportOptions, { scanned, read }: FrictionReportInputs): SessionFrictionReport;
|
|
15
|
-
export declare function buildSessionFrictionReport(sessionId: string, options: SessionFrictionReportOptions): Promise<SessionFrictionReport>;
|
|
16
|
-
export {};
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { eventLimit, readFrictionLog, requireDirectory } from "./log.mjs";
|
|
2
|
-
import { buildCiFailuresSection, getConformingGroups, incompleteCiSection } from "./ci-failures.mjs";
|
|
3
|
-
import { errorMessage, scanJournal } from "./journal.mjs";
|
|
4
|
-
import { buildSandboxSection } from "./sandbox.mjs";
|
|
5
|
-
import { validateSessionId } from "./session-id.mjs";
|
|
6
|
-
function sandboxMarkdown(friction, options) {
|
|
7
|
-
if (friction.status === 'events')
|
|
8
|
-
return `${friction.truncated ? `## Capture Coverage\nStatus: partial\nDropped records: ${friction.droppedCount}\n\n` : ''}${buildSandboxSection(friction.events, options.redact, options.budgets?.sandboxBytes)}`;
|
|
9
|
-
if (friction.status === 'empty' && friction.truncated)
|
|
10
|
-
return `## Sandbox & Permission Audit\nStatus: unavailable (partial capture; dropped ${friction.droppedCount} records)`;
|
|
11
|
-
const status = friction.status === 'empty' ? 'none observed' : 'unavailable (no friction log)';
|
|
12
|
-
return `## Sandbox & Permission Audit\nStatus: ${status}`;
|
|
13
|
-
}
|
|
14
|
-
function reportFromLog(sessionId, logOptions, journal, read) {
|
|
15
|
-
if ('result' in read) {
|
|
16
|
-
const friction = read.result;
|
|
17
|
-
const coverage = {
|
|
18
|
-
journalStatus: journal.status === 'unreachable'
|
|
19
|
-
? 'unavailable'
|
|
20
|
-
: journal.truncated
|
|
21
|
-
? 'partial'
|
|
22
|
-
: 'complete',
|
|
23
|
-
frictionStatus: friction.status,
|
|
24
|
-
truncated: friction.status !== 'absent' && friction.truncated === true,
|
|
25
|
-
...(friction.status === 'absent' ? {} : { droppedCount: friction.droppedCount ?? 0 }),
|
|
26
|
-
};
|
|
27
|
-
if (journal.status === 'ok' && journal.truncated)
|
|
28
|
-
return {
|
|
29
|
-
coverage,
|
|
30
|
-
markdown: incompleteCiSection(journal.markdownBlocks) +
|
|
31
|
-
'\n\n' +
|
|
32
|
-
sandboxMarkdown(friction, logOptions),
|
|
33
|
-
};
|
|
34
|
-
const markdown = buildCiFailuresSection(sessionId, journal, friction.status);
|
|
35
|
-
return { coverage, markdown: `${markdown}\n\n${sandboxMarkdown(friction, logOptions)}` };
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
const ciMarkdown = journal.status === 'ok' && journal.truncated
|
|
39
|
-
? incompleteCiSection(journal.markdownBlocks)
|
|
40
|
-
: journal.status === 'ok' && journal.markdownBlocks.length === 0
|
|
41
|
-
? '## CI Failures\nStatus: unavailable (friction log unreadable)'
|
|
42
|
-
: buildCiFailuresSection(sessionId, journal, 'empty');
|
|
43
|
-
return {
|
|
44
|
-
coverage: {
|
|
45
|
-
journalStatus: journal.status === 'unreachable'
|
|
46
|
-
? 'unavailable'
|
|
47
|
-
: journal.truncated
|
|
48
|
-
? 'partial'
|
|
49
|
-
: 'complete',
|
|
50
|
-
frictionStatus: 'unreadable',
|
|
51
|
-
truncated: true,
|
|
52
|
-
},
|
|
53
|
-
markdown: `${ciMarkdown}\n\n## Sandbox & Permission Audit\nStatus: unavailable (friction log unreadable)`,
|
|
54
|
-
diagnostic: errorMessage(read.error),
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
export async function loadFrictionReportInputs(sessionId, options) {
|
|
59
|
-
validateSessionId(sessionId);
|
|
60
|
-
requireDirectory(options.directory);
|
|
61
|
-
eventLimit(options.maxEvents);
|
|
62
|
-
const scanned = await scanJournal(sessionId, options.journalLoader);
|
|
63
|
-
try {
|
|
64
|
-
return { scanned, read: { result: readFrictionLog(sessionId, options) } };
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
return { scanned, read: { error } };
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
export function renderFrictionReport(sessionId, options, { scanned, read }) {
|
|
71
|
-
const journal = scanned.status === 'unreachable'
|
|
72
|
-
? scanned
|
|
73
|
-
: scanned.status === 'not-found'
|
|
74
|
-
? { status: 'ok', markdownBlocks: [], truncated: false }
|
|
75
|
-
: {
|
|
76
|
-
status: 'ok',
|
|
77
|
-
markdownBlocks: getConformingGroups(scanned.entries, options.redact, options.budgets?.ciBytes),
|
|
78
|
-
truncated: scanned.truncated,
|
|
79
|
-
};
|
|
80
|
-
const report = reportFromLog(sessionId, options, journal, read);
|
|
81
|
-
if (journal.status === 'unreachable') {
|
|
82
|
-
const diagnostic = report.diagnostic
|
|
83
|
-
? `${journal.diagnostic}; ${report.diagnostic}`
|
|
84
|
-
: journal.diagnostic;
|
|
85
|
-
return { ...report, diagnostic };
|
|
86
|
-
}
|
|
87
|
-
return report;
|
|
88
|
-
}
|
|
89
|
-
export async function buildSessionFrictionReport(sessionId, options) {
|
|
90
|
-
return renderFrictionReport(sessionId, options, await loadFrictionReportInputs(sessionId, options));
|
|
91
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { byteLength, fitFields, selectWithin } from "./audit-fit.mjs";
|
|
2
|
-
import { markdownAuditUnits } from "./text.mjs";
|
|
3
|
-
const SANDBOX_SECTION_HEADER = '## Sandbox & Permission Audit';
|
|
4
|
-
const SANDBOX_SECTION_MAX_BYTES = 3_000;
|
|
5
|
-
// Room for the `- kind (n)` group lines, which are rendered outside the per-event budget.
|
|
6
|
-
const GROUP_LINES_BYTES = 120;
|
|
7
|
-
// Below this an event line would keep little beyond its timestamp, so it is omitted and counted.
|
|
8
|
-
const MIN_EVENT_SHARE_BYTES = 120;
|
|
9
|
-
// Event line fields: command prefix, detail, timestamp. Cut detail first, the timestamp never.
|
|
10
|
-
const SHRINK_ORDER = [1, 0];
|
|
11
|
-
const KIND_ORDER = [
|
|
12
|
-
'sandbox-escalation',
|
|
13
|
-
'sandbox-failure',
|
|
14
|
-
'ambiguous-failure',
|
|
15
|
-
];
|
|
16
|
-
/** Reports dropped events as a group in the same shape as the rest, so omission is never silent. */
|
|
17
|
-
function omissionGroup(count) {
|
|
18
|
-
return `- omitted (${count})\n - see journal — ${count} events omitted to fit the size limit — n/a`;
|
|
19
|
-
}
|
|
20
|
-
export function buildSandboxSection(events, redact, maxBytes = SANDBOX_SECTION_MAX_BYTES) {
|
|
21
|
-
const observations = events.flatMap((event) => event.failure
|
|
22
|
-
? [event, { ...event, kind: event.failure.kind, detail: event.failure.detail }]
|
|
23
|
-
: [event]);
|
|
24
|
-
const render = (event, budgetBytes) => {
|
|
25
|
-
const outcome = event.kind === 'sandbox-escalation' ? ` — outcome: ${event.outcome ?? 'unknown'}` : '';
|
|
26
|
-
const [prefix, detail, timestamp] = fitFields([event.commandPrefix, event.detail, event.timestamp].map((field) => markdownAuditUnits(field, redact)), byteLength(` - — — ${outcome}`), budgetBytes, SHRINK_ORDER);
|
|
27
|
-
return ` - ${prefix} — ${detail} — ${timestamp}${outcome}`;
|
|
28
|
-
};
|
|
29
|
-
const total = Math.max(0, Math.min(maxBytes, SANDBOX_SECTION_MAX_BYTES) - GROUP_LINES_BYTES);
|
|
30
|
-
const { rendered, omitted } = selectWithin(observations, total, byteLength(omissionGroup(observations.length)) + 1, 1, MIN_EVENT_SHARE_BYTES, render);
|
|
31
|
-
const kept = observations
|
|
32
|
-
.slice(0, rendered.length)
|
|
33
|
-
.map((event, i) => ({ event, line: rendered[i] }));
|
|
34
|
-
const groups = KIND_ORDER.map((kind) => {
|
|
35
|
-
const selected = kept.filter(({ event }) => event.kind === kind);
|
|
36
|
-
return selected.length
|
|
37
|
-
? [`- ${kind} (${selected.length})`, ...selected.map(({ line }) => line)].join('\n')
|
|
38
|
-
: undefined;
|
|
39
|
-
}).filter((group) => group !== undefined);
|
|
40
|
-
if (omitted)
|
|
41
|
-
groups.push(omissionGroup(omitted));
|
|
42
|
-
return `${SANDBOX_SECTION_HEADER}\nEvents observed: ${observations.length}\n\n${groups.join('\n')}`;
|
|
43
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
export type FrictionEventKind = 'sandbox-escalation' | 'sandbox-failure' | 'ambiguous-failure';
|
|
2
|
-
export type FrictionEvent = {
|
|
3
|
-
kind: FrictionEventKind;
|
|
4
|
-
timestamp: string;
|
|
5
|
-
commandPrefix: string;
|
|
6
|
-
detail: string;
|
|
7
|
-
outcome?: 'requested' | 'approved' | 'denied' | 'unknown';
|
|
8
|
-
failure?: {
|
|
9
|
-
kind: 'sandbox-failure' | 'ambiguous-failure';
|
|
10
|
-
detail: string;
|
|
11
|
-
};
|
|
12
|
-
};
|
|
13
|
-
export type ToolResultObservation = {
|
|
14
|
-
type: 'tool-result';
|
|
15
|
-
command: string;
|
|
16
|
-
commandWrappers?: string[];
|
|
17
|
-
permissionOutcome?: 'approved' | 'denied';
|
|
18
|
-
escalationDetail?: string;
|
|
19
|
-
structuredStderr?: string;
|
|
20
|
-
};
|
|
21
|
-
export type PermissionRequestObservation = {
|
|
22
|
-
type: 'permission-request';
|
|
23
|
-
command: string;
|
|
24
|
-
commandWrappers?: string[];
|
|
25
|
-
};
|
|
26
|
-
export type FrictionObservation = ToolResultObservation | PermissionRequestObservation;
|
|
27
|
-
type FrictionLogCoverage = {
|
|
28
|
-
truncated: true;
|
|
29
|
-
droppedCount: number;
|
|
30
|
-
} | {
|
|
31
|
-
truncated?: false;
|
|
32
|
-
droppedCount?: never;
|
|
33
|
-
};
|
|
34
|
-
export type FrictionLogReadResult = {
|
|
35
|
-
status: 'absent';
|
|
36
|
-
} | ({
|
|
37
|
-
status: 'empty';
|
|
38
|
-
} & FrictionLogCoverage) | ({
|
|
39
|
-
status: 'events';
|
|
40
|
-
events: FrictionEvent[];
|
|
41
|
-
} & FrictionLogCoverage);
|
|
42
|
-
export type FrictionLogOptions = {
|
|
43
|
-
directory: string;
|
|
44
|
-
maxEvents?: number;
|
|
45
|
-
};
|
|
46
|
-
export type JournalEntry = {
|
|
47
|
-
data?: {
|
|
48
|
-
type?: unknown;
|
|
49
|
-
markdown?: unknown;
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
|
-
export type JournalLoadResult = {
|
|
53
|
-
status: 'ok';
|
|
54
|
-
entries: Iterable<JournalEntry> | AsyncIterable<JournalEntry>;
|
|
55
|
-
} | {
|
|
56
|
-
status: 'not-found';
|
|
57
|
-
};
|
|
58
|
-
export type JournalLoader = (sessionId: string) => JournalLoadResult | Promise<JournalLoadResult>;
|
|
59
|
-
export type SessionFrictionCoverage = {
|
|
60
|
-
journalStatus: 'complete' | 'partial' | 'unavailable';
|
|
61
|
-
frictionStatus: 'absent' | 'empty' | 'events' | 'unreadable';
|
|
62
|
-
truncated: boolean;
|
|
63
|
-
droppedCount?: number;
|
|
64
|
-
};
|
|
65
|
-
export type SessionFrictionReport = {
|
|
66
|
-
coverage: SessionFrictionCoverage;
|
|
67
|
-
markdown: string;
|
|
68
|
-
diagnostic?: string;
|
|
69
|
-
};
|
|
70
|
-
export type SessionFrictionReportOptions = FrictionLogOptions & {
|
|
71
|
-
journalLoader: JournalLoader;
|
|
72
|
-
/** Redacts raw field text before Markdown escaping, which would otherwise break matching. */
|
|
73
|
-
redact?: (value: string) => string;
|
|
74
|
-
/** Byte allowances for the CI and sandbox sections, as maximums over the built-in budgets. */
|
|
75
|
-
budgets?: {
|
|
76
|
-
ciBytes: number;
|
|
77
|
-
sandboxBytes: number;
|
|
78
|
-
};
|
|
79
|
-
};
|
|
80
|
-
export {};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|