vouchington-tooling 0.1.4 → 0.1.6
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 +36 -0
- package/dist/cli/index.mjs +4 -0
- package/dist/cli/parse.d.mts +1 -1
- package/dist/cli/parse.mjs +1 -0
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +3 -1
- package/dist/gha-post-review/post.mjs +2 -9
- package/dist/gha-review-payload/index.d.mts +1 -1
- package/dist/gha-review-payload/index.mjs +1 -1
- package/dist/gha-review-payload/payload.d.mts +1 -3
- package/dist/gha-review-payload/payload.mjs +15 -31
- package/dist/gha-review-payload/remap.d.mts +1 -1
- package/dist/gha-review-payload/remap.mjs +7 -16
- package/dist/index.d.mts +3 -1
- package/dist/index.mjs +2 -1
- package/dist/retrospective-transcript/codex-messages.d.mts +8 -0
- package/dist/retrospective-transcript/codex-messages.mjs +66 -0
- package/dist/retrospective-transcript/codex.mjs +9 -4
- package/dist/session-friction/ci-failures.d.mts +11 -0
- package/dist/session-friction/ci-failures.mjs +78 -0
- package/dist/session-friction/classify.d.mts +2 -0
- package/dist/session-friction/classify.mjs +87 -0
- package/dist/session-friction/directory.d.mts +1 -0
- package/dist/session-friction/directory.mjs +71 -0
- package/dist/session-friction/index.d.mts +6 -0
- package/dist/session-friction/index.mjs +5 -0
- package/dist/session-friction/lock.d.mts +1 -0
- package/dist/session-friction/lock.mjs +193 -0
- package/dist/session-friction/log.d.mts +8 -0
- package/dist/session-friction/log.mjs +195 -0
- package/dist/session-friction/normalize.d.mts +1 -0
- package/dist/session-friction/normalize.mjs +174 -0
- package/dist/session-friction/report.d.mts +2 -0
- package/dist/session-friction/report.mjs +113 -0
- package/dist/session-friction/sandbox.d.mts +2 -0
- package/dist/session-friction/sandbox.mjs +15 -0
- package/dist/session-friction/session-id.d.mts +2 -0
- package/dist/session-friction/session-id.mjs +17 -0
- package/dist/session-friction/text.d.mts +5 -0
- package/dist/session-friction/text.mjs +45 -0
- package/dist/session-friction/types.d.mts +52 -0
- package/dist/session-friction/types.mjs +1 -0
- package/dist/session-friction/utf8.d.mts +1 -0
- package/dist/session-friction/utf8.mjs +8 -0
- package/package.json +7 -2
- package/scripts/gha/download-optional-run-artifacts.sh +126 -0
- package/scripts/gha/install-github-release.sh +17 -4
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
const MAX_COMMAND_TOKEN_LENGTH = 40;
|
|
2
|
+
const MAX_COMMAND_LENGTH = 100_000;
|
|
3
|
+
const MAX_COMMAND_WRAPPERS = 16;
|
|
4
|
+
const REDACTED_TOKEN = '[REDACTED]';
|
|
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 ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
8
|
+
const PACKAGE_RUNNERS = new Set(['npm', 'npx', 'pnpm', 'pnpx', 'yarn']);
|
|
9
|
+
const GIT_OPTIONS_WITH_ARGS = new Set(['-C', '-c', '--git-dir', '--work-tree']);
|
|
10
|
+
const ENV_OPTIONS_WITH_ARGS = new Set(['-C', '--chdir', '-S', '--split-string', '-u', '--unset']);
|
|
11
|
+
const ENV_OPTIONS_WITHOUT_ARGS = new Set([
|
|
12
|
+
'-i',
|
|
13
|
+
'--ignore-environment',
|
|
14
|
+
'-0',
|
|
15
|
+
'--null',
|
|
16
|
+
'-v',
|
|
17
|
+
'--debug',
|
|
18
|
+
]);
|
|
19
|
+
const GIT_OPTIONS_WITHOUT_ARGS = new Set([
|
|
20
|
+
'-v',
|
|
21
|
+
'--version',
|
|
22
|
+
'-h',
|
|
23
|
+
'--help',
|
|
24
|
+
'--exec-path',
|
|
25
|
+
'--html-path',
|
|
26
|
+
'--man-path',
|
|
27
|
+
'--info-path',
|
|
28
|
+
'-p',
|
|
29
|
+
'--paginate',
|
|
30
|
+
'-P',
|
|
31
|
+
'--no-pager',
|
|
32
|
+
'--no-replace-objects',
|
|
33
|
+
'--bare',
|
|
34
|
+
]);
|
|
35
|
+
function splitCommand(command) {
|
|
36
|
+
const result = [];
|
|
37
|
+
let segment = [];
|
|
38
|
+
let word = '';
|
|
39
|
+
let quote;
|
|
40
|
+
let escaped = false;
|
|
41
|
+
const flushWord = () => {
|
|
42
|
+
if (word)
|
|
43
|
+
segment.push(word);
|
|
44
|
+
word = '';
|
|
45
|
+
};
|
|
46
|
+
const flushSegment = () => {
|
|
47
|
+
flushWord();
|
|
48
|
+
if (segment.length)
|
|
49
|
+
result.push(segment);
|
|
50
|
+
segment = [];
|
|
51
|
+
};
|
|
52
|
+
for (const char of command) {
|
|
53
|
+
if (escaped) {
|
|
54
|
+
word += char;
|
|
55
|
+
escaped = false;
|
|
56
|
+
}
|
|
57
|
+
else if (char === '\\' && quote !== "'") {
|
|
58
|
+
escaped = true;
|
|
59
|
+
}
|
|
60
|
+
else if (quote) {
|
|
61
|
+
if (char === quote)
|
|
62
|
+
quote = undefined;
|
|
63
|
+
else
|
|
64
|
+
word += char;
|
|
65
|
+
}
|
|
66
|
+
else if (char === '"' || char === "'")
|
|
67
|
+
quote = char;
|
|
68
|
+
else if (';&|<>\n\r'.includes(char))
|
|
69
|
+
flushSegment();
|
|
70
|
+
else if (/\s/.test(char))
|
|
71
|
+
flushWord();
|
|
72
|
+
else
|
|
73
|
+
word += char;
|
|
74
|
+
}
|
|
75
|
+
if (escaped)
|
|
76
|
+
word += '\\';
|
|
77
|
+
flushSegment();
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
function redact(token) {
|
|
81
|
+
return token.length > MAX_COMMAND_TOKEN_LENGTH ||
|
|
82
|
+
CREDENTIAL_OPTION.test(token) ||
|
|
83
|
+
URL_USERINFO.test(token)
|
|
84
|
+
? REDACTED_TOKEN
|
|
85
|
+
: token;
|
|
86
|
+
}
|
|
87
|
+
function stripAssignments(tokens) {
|
|
88
|
+
let index = 0;
|
|
89
|
+
while (index < tokens.length && ENV_ASSIGNMENT.test(tokens[index]))
|
|
90
|
+
index++;
|
|
91
|
+
const wrapper = tokens[index];
|
|
92
|
+
// Assignments without an executable are represented by a safe sentinel.
|
|
93
|
+
if (wrapper !== 'env' && !wrapper?.endsWith('/env'))
|
|
94
|
+
return index < tokens.length ? tokens.slice(index) : index ? [REDACTED_TOKEN] : tokens;
|
|
95
|
+
index++;
|
|
96
|
+
while (index < tokens.length) {
|
|
97
|
+
const option = tokens[index];
|
|
98
|
+
if (option === '--') {
|
|
99
|
+
index++;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
const optionName = option.split('=', 1)[0];
|
|
103
|
+
const splitStringOption = optionName === '-S' || (optionName.length >= 5 && '--split-string'.startsWith(optionName));
|
|
104
|
+
if (splitStringOption)
|
|
105
|
+
return [REDACTED_TOKEN];
|
|
106
|
+
const abbreviatedArgument = optionName.length >= 5 && '--chdir'.startsWith(optionName);
|
|
107
|
+
if (ENV_OPTIONS_WITH_ARGS.has(optionName) || abbreviatedArgument) {
|
|
108
|
+
index += option.includes('=') ? 1 : 2;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (ENV_OPTIONS_WITHOUT_ARGS.has(option)) {
|
|
112
|
+
index++;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (option.startsWith('-'))
|
|
116
|
+
return [REDACTED_TOKEN];
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
while (index < tokens.length && ENV_ASSIGNMENT.test(tokens[index]))
|
|
120
|
+
index++;
|
|
121
|
+
if (index < tokens.length)
|
|
122
|
+
return tokens.slice(index);
|
|
123
|
+
return tokens.some((token) => ENV_ASSIGNMENT.test(token)) ? [REDACTED_TOKEN] : [];
|
|
124
|
+
}
|
|
125
|
+
function stripGitOptions(tokens) {
|
|
126
|
+
let index = 0;
|
|
127
|
+
while (index < tokens.length) {
|
|
128
|
+
const token = tokens[index];
|
|
129
|
+
if (token === '--')
|
|
130
|
+
return tokens.slice(index + 1);
|
|
131
|
+
if (GIT_OPTIONS_WITH_ARGS.has(token)) {
|
|
132
|
+
if (index + 1 >= tokens.length)
|
|
133
|
+
return [REDACTED_TOKEN];
|
|
134
|
+
index += 2;
|
|
135
|
+
}
|
|
136
|
+
else if (GIT_OPTIONS_WITHOUT_ARGS.has(token) || /^--[a-z0-9-]+=/i.test(token))
|
|
137
|
+
index++;
|
|
138
|
+
else
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
return tokens.slice(index);
|
|
142
|
+
}
|
|
143
|
+
function packageRunner(token) {
|
|
144
|
+
return [...PACKAGE_RUNNERS].some((runner) => token === runner || token.endsWith(`/${runner}`));
|
|
145
|
+
}
|
|
146
|
+
function normalizeSegment(tokens) {
|
|
147
|
+
const [first, ...rest] = tokens;
|
|
148
|
+
if (!first)
|
|
149
|
+
return '';
|
|
150
|
+
if (packageRunner(first) && (rest[0] === 'run' || rest[0] === 'exec') && rest[1])
|
|
151
|
+
return `${redact(first)} ${rest[0]} ${redact(rest[1])}`;
|
|
152
|
+
const effective = first === 'git' || first.endsWith('/git') ? stripGitOptions(rest) : rest;
|
|
153
|
+
return effective[0] ? `${redact(first)} ${redact(effective[0])}` : redact(first);
|
|
154
|
+
}
|
|
155
|
+
export function normalizeCommandPrefix(command, wrappersToStrip = []) {
|
|
156
|
+
if (command.length > MAX_COMMAND_LENGTH)
|
|
157
|
+
return '[REDACTED: command too long]';
|
|
158
|
+
const segments = splitCommand(command);
|
|
159
|
+
let index = 0;
|
|
160
|
+
while (index < segments.length - 1 && stripAssignments(segments[index])[0] === 'cd')
|
|
161
|
+
index++;
|
|
162
|
+
const tokens = stripAssignments(segments[index] ?? []);
|
|
163
|
+
const wrapperValues = Array.isArray(wrappersToStrip) ? wrappersToStrip : [];
|
|
164
|
+
const wrapperNames = new Set(wrapperValues
|
|
165
|
+
.slice(0, MAX_COMMAND_WRAPPERS)
|
|
166
|
+
.filter((value) => typeof value === 'string' && value.length > 0 && value.length <= MAX_COMMAND_TOKEN_LENGTH));
|
|
167
|
+
let wrappers = 0;
|
|
168
|
+
while (tokens[wrappers] && wrapperNames.has(tokens[wrappers]))
|
|
169
|
+
wrappers++;
|
|
170
|
+
if (!wrappers)
|
|
171
|
+
return normalizeSegment(tokens);
|
|
172
|
+
const nested = normalizeSegment(stripAssignments(tokens.slice(wrappers)));
|
|
173
|
+
return nested ? `${redact(tokens[0])} ${nested}` : redact(tokens[0]);
|
|
174
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { eventLimit, readFrictionLog, requireDirectory } from './log.mjs';
|
|
2
|
+
import { buildCiFailuresSection, getConformingGroups } from './ci-failures.mjs';
|
|
3
|
+
import { buildSandboxSection } from './sandbox.mjs';
|
|
4
|
+
import { validateSessionId } from './session-id.mjs';
|
|
5
|
+
const JOURNAL_ENTRY_MAX_COUNT = 500;
|
|
6
|
+
const JOURNAL_MARKDOWN_MAX_BYTES = 10_000;
|
|
7
|
+
const JOURNAL_TOTAL_MAX_BYTES = 1_000_000;
|
|
8
|
+
function journalData(entry) {
|
|
9
|
+
try {
|
|
10
|
+
const data = entry?.data;
|
|
11
|
+
return data ? { type: data.type, markdown: data.markdown } : undefined;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function collectEntries(entries) {
|
|
18
|
+
const result = [];
|
|
19
|
+
let consumed = 0;
|
|
20
|
+
let inspectedBytes = 0;
|
|
21
|
+
let retainedBytes = 0;
|
|
22
|
+
let truncated = false;
|
|
23
|
+
for await (const entry of entries) {
|
|
24
|
+
consumed++;
|
|
25
|
+
const data = journalData(entry);
|
|
26
|
+
const markdown = data?.markdown;
|
|
27
|
+
if (data?.type === 'journal' && typeof markdown === 'string') {
|
|
28
|
+
const bytes = Buffer.byteLength(markdown);
|
|
29
|
+
inspectedBytes += bytes;
|
|
30
|
+
if (bytes > JOURNAL_MARKDOWN_MAX_BYTES)
|
|
31
|
+
truncated = true;
|
|
32
|
+
if (bytes <= JOURNAL_MARKDOWN_MAX_BYTES && retainedBytes + bytes <= JOURNAL_TOTAL_MAX_BYTES) {
|
|
33
|
+
result.push({ data: { type: 'journal', markdown } });
|
|
34
|
+
retainedBytes += bytes;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (consumed >= JOURNAL_ENTRY_MAX_COUNT ||
|
|
38
|
+
inspectedBytes >= JOURNAL_TOTAL_MAX_BYTES ||
|
|
39
|
+
retainedBytes >= JOURNAL_TOTAL_MAX_BYTES) {
|
|
40
|
+
truncated = true;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { entries: result, truncated };
|
|
45
|
+
}
|
|
46
|
+
function errorMessage(error) {
|
|
47
|
+
try {
|
|
48
|
+
return error instanceof Error ? error.message : String(error);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return '[unprintable error]';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function sandboxMarkdown(friction) {
|
|
55
|
+
if (friction.status === 'events')
|
|
56
|
+
return buildSandboxSection(friction.events);
|
|
57
|
+
const status = friction.status === 'empty' ? 'none observed' : 'unavailable (no friction log)';
|
|
58
|
+
return `## Sandbox & Permission Audit\nStatus: ${status}`;
|
|
59
|
+
}
|
|
60
|
+
function reportFromLog(sessionId, logOptions, journal) {
|
|
61
|
+
try {
|
|
62
|
+
const friction = readFrictionLog(sessionId, logOptions);
|
|
63
|
+
if (journal.status === 'ok' && journal.truncated)
|
|
64
|
+
return {
|
|
65
|
+
markdown: '## CI Failures\nStatus: unavailable (journal scan incomplete)\n\n' +
|
|
66
|
+
sandboxMarkdown(friction),
|
|
67
|
+
};
|
|
68
|
+
const markdown = buildCiFailuresSection(sessionId, journal, friction.status);
|
|
69
|
+
return { markdown: `${markdown}\n\n${sandboxMarkdown(friction)}` };
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
const ciMarkdown = journal.status === 'ok' && journal.truncated
|
|
73
|
+
? '## CI Failures\nStatus: unavailable (journal scan incomplete)'
|
|
74
|
+
: journal.status === 'ok' && journal.markdownBlocks.length === 0
|
|
75
|
+
? '## CI Failures\nStatus: unavailable (friction log unreadable)'
|
|
76
|
+
: buildCiFailuresSection(sessionId, journal, 'empty');
|
|
77
|
+
return {
|
|
78
|
+
markdown: `${ciMarkdown}\n\n## Sandbox & Permission Audit\nStatus: unavailable (friction log unreadable)`,
|
|
79
|
+
diagnostic: errorMessage(error),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export async function buildSessionFrictionReport(sessionId, options) {
|
|
84
|
+
validateSessionId(sessionId);
|
|
85
|
+
requireDirectory(options.directory);
|
|
86
|
+
eventLimit(options.maxEvents);
|
|
87
|
+
let journal;
|
|
88
|
+
try {
|
|
89
|
+
const loaded = await options.journalLoader(sessionId);
|
|
90
|
+
if (loaded.status === 'not-found')
|
|
91
|
+
journal = { status: 'ok', markdownBlocks: [], truncated: false };
|
|
92
|
+
else {
|
|
93
|
+
const collected = await collectEntries(loaded.entries);
|
|
94
|
+
journal = {
|
|
95
|
+
status: 'ok',
|
|
96
|
+
markdownBlocks: getConformingGroups(collected.entries),
|
|
97
|
+
truncated: collected.truncated,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
const diagnostic = errorMessage(error);
|
|
103
|
+
journal = { status: 'unreachable', diagnostic };
|
|
104
|
+
}
|
|
105
|
+
const report = reportFromLog(sessionId, options, journal);
|
|
106
|
+
if (journal.status === 'unreachable') {
|
|
107
|
+
const diagnostic = report.diagnostic
|
|
108
|
+
? `${journal.diagnostic}; ${report.diagnostic}`
|
|
109
|
+
: journal.diagnostic;
|
|
110
|
+
return { ...report, diagnostic };
|
|
111
|
+
}
|
|
112
|
+
return report;
|
|
113
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { markdownAuditText } from './text.mjs';
|
|
2
|
+
const SANDBOX_SECTION_HEADER = '## Sandbox & Permission Audit';
|
|
3
|
+
const KIND_ORDER = ['sandbox-escalation', 'sandbox-failure'];
|
|
4
|
+
export function buildSandboxSection(events) {
|
|
5
|
+
const groups = KIND_ORDER.map((kind) => {
|
|
6
|
+
const selected = events.filter((event) => event.kind === kind);
|
|
7
|
+
return selected.length
|
|
8
|
+
? [
|
|
9
|
+
`- ${kind} (${selected.length})`,
|
|
10
|
+
...selected.map((event) => ` - ${markdownAuditText(event.commandPrefix)} — ${markdownAuditText(event.detail)} — ${markdownAuditText(event.timestamp)}`),
|
|
11
|
+
].join('\n')
|
|
12
|
+
: undefined;
|
|
13
|
+
}).filter((group) => group !== undefined);
|
|
14
|
+
return `${SANDBOX_SECTION_HEADER}\nEvents observed: ${events.length}\n\n${groups.join('\n')}`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { isWellFormedUnicode, normalizeAuditText } from './text.mjs';
|
|
3
|
+
const SESSION_ID_MAX_LENGTH = 4096;
|
|
4
|
+
export function validateSessionId(sessionId) {
|
|
5
|
+
if (typeof sessionId !== 'string')
|
|
6
|
+
throw new Error('sessionId must be a string');
|
|
7
|
+
if (sessionId.length > SESSION_ID_MAX_LENGTH)
|
|
8
|
+
throw new Error('sessionId is too long');
|
|
9
|
+
if (!isWellFormedUnicode(sessionId))
|
|
10
|
+
throw new Error('sessionId must be well-formed Unicode');
|
|
11
|
+
if (normalizeAuditText(sessionId) === '')
|
|
12
|
+
throw new Error('sessionId must be non-empty');
|
|
13
|
+
}
|
|
14
|
+
export function sanitizeSessionId(sessionId) {
|
|
15
|
+
validateSessionId(sessionId);
|
|
16
|
+
return createHash('sha256').update(sessionId).digest('hex');
|
|
17
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function normalizeAuditText(value: string): string;
|
|
2
|
+
export declare function boundedText(value: string, maximum: number): string;
|
|
3
|
+
export declare function isWellFormedUnicode(value: string): boolean;
|
|
4
|
+
export declare function isSafeAuditText(value: unknown): value is string;
|
|
5
|
+
export declare function markdownAuditText(value: string): string;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const CONTROL_CHARACTERS = /[\p{Cc}\p{Cf}]+/gu;
|
|
2
|
+
const MARKDOWN_CHARACTER = /\\|`|\*|_|~|\[|\]|<|>|#|-|\+|!|\||&/;
|
|
3
|
+
const MARKDOWN_AUDIT_MAX_LENGTH = 120;
|
|
4
|
+
export function normalizeAuditText(value) {
|
|
5
|
+
return value.replace(CONTROL_CHARACTERS, ' ').trim();
|
|
6
|
+
}
|
|
7
|
+
export function boundedText(value, maximum) {
|
|
8
|
+
let end = Math.min(value.length, maximum);
|
|
9
|
+
if (end < value.length &&
|
|
10
|
+
/[\uD800-\uDBFF]/.test(value[end - 1]) &&
|
|
11
|
+
/[\uDC00-\uDFFF]/.test(value[end]))
|
|
12
|
+
end--;
|
|
13
|
+
return value.slice(0, end);
|
|
14
|
+
}
|
|
15
|
+
export function isWellFormedUnicode(value) {
|
|
16
|
+
for (let index = 0; index < value.length; index++) {
|
|
17
|
+
const code = value.charCodeAt(index);
|
|
18
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
19
|
+
const next = value.charCodeAt(index + 1);
|
|
20
|
+
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
21
|
+
return false;
|
|
22
|
+
index++;
|
|
23
|
+
}
|
|
24
|
+
else if (code >= 0xdc00 && code <= 0xdfff)
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
export function isSafeAuditText(value) {
|
|
30
|
+
return (typeof value === 'string' &&
|
|
31
|
+
value !== '' &&
|
|
32
|
+
isWellFormedUnicode(value) &&
|
|
33
|
+
value === normalizeAuditText(value));
|
|
34
|
+
}
|
|
35
|
+
export function markdownAuditText(value) {
|
|
36
|
+
// Escaping is atomic, so the result may be shorter than the maximum.
|
|
37
|
+
let result = '';
|
|
38
|
+
for (const character of normalizeAuditText(value)) {
|
|
39
|
+
const escaped = MARKDOWN_CHARACTER.test(character) ? `\\${character}` : character;
|
|
40
|
+
if (result.length + escaped.length > MARKDOWN_AUDIT_MAX_LENGTH)
|
|
41
|
+
break;
|
|
42
|
+
result += escaped;
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type FrictionEventKind = 'sandbox-escalation' | 'sandbox-failure';
|
|
2
|
+
export type FrictionEvent = {
|
|
3
|
+
kind: FrictionEventKind;
|
|
4
|
+
timestamp: string;
|
|
5
|
+
commandPrefix: string;
|
|
6
|
+
detail: string;
|
|
7
|
+
};
|
|
8
|
+
export type ToolResultObservation = {
|
|
9
|
+
type: 'tool-result';
|
|
10
|
+
command: string;
|
|
11
|
+
commandWrappers?: string[];
|
|
12
|
+
escalationDetail?: string;
|
|
13
|
+
structuredStderr?: string;
|
|
14
|
+
};
|
|
15
|
+
export type PermissionRequestObservation = {
|
|
16
|
+
type: 'permission-request';
|
|
17
|
+
command: string;
|
|
18
|
+
commandWrappers?: string[];
|
|
19
|
+
};
|
|
20
|
+
export type FrictionObservation = ToolResultObservation | PermissionRequestObservation;
|
|
21
|
+
export type FrictionLogReadResult = {
|
|
22
|
+
status: 'absent';
|
|
23
|
+
} | {
|
|
24
|
+
status: 'empty';
|
|
25
|
+
} | {
|
|
26
|
+
status: 'events';
|
|
27
|
+
events: FrictionEvent[];
|
|
28
|
+
};
|
|
29
|
+
export type FrictionLogOptions = {
|
|
30
|
+
directory: string;
|
|
31
|
+
maxEvents?: number;
|
|
32
|
+
};
|
|
33
|
+
export type JournalEntry = {
|
|
34
|
+
data?: {
|
|
35
|
+
type?: unknown;
|
|
36
|
+
markdown?: unknown;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
export type JournalLoadResult = {
|
|
40
|
+
status: 'ok';
|
|
41
|
+
entries: Iterable<JournalEntry> | AsyncIterable<JournalEntry>;
|
|
42
|
+
} | {
|
|
43
|
+
status: 'not-found';
|
|
44
|
+
};
|
|
45
|
+
export type JournalLoader = (sessionId: string) => JournalLoadResult | Promise<JournalLoadResult>;
|
|
46
|
+
export type SessionFrictionReport = {
|
|
47
|
+
markdown: string;
|
|
48
|
+
diagnostic?: string;
|
|
49
|
+
};
|
|
50
|
+
export type SessionFrictionReportOptions = FrictionLogOptions & {
|
|
51
|
+
journalLoader: JournalLoader;
|
|
52
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function decodeUtf8(value: Uint8Array): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vouchington-tooling",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Vouchington CLI and extractable tooling libraries.",
|
|
5
5
|
"homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -41,6 +41,11 @@
|
|
|
41
41
|
"import": "./dist/retrospective-transcript/index.mjs",
|
|
42
42
|
"default": "./dist/retrospective-transcript/index.mjs"
|
|
43
43
|
},
|
|
44
|
+
"./session-friction": {
|
|
45
|
+
"types": "./dist/session-friction/index.d.mts",
|
|
46
|
+
"import": "./dist/session-friction/index.mjs",
|
|
47
|
+
"default": "./dist/session-friction/index.mjs"
|
|
48
|
+
},
|
|
44
49
|
"./sql-ast": {
|
|
45
50
|
"types": "./dist/sql-ast/index.d.mts",
|
|
46
51
|
"import": "./dist/sql-ast/index.mjs",
|
|
@@ -223,7 +228,7 @@
|
|
|
223
228
|
},
|
|
224
229
|
"dependencies": {
|
|
225
230
|
"csv-parse": "7.0.2",
|
|
226
|
-
"csv-stringify": "6.8.
|
|
231
|
+
"csv-stringify": "6.8.3",
|
|
227
232
|
"dockerfile-ast": "0.7.1",
|
|
228
233
|
"picomatch": "4.0.5",
|
|
229
234
|
"yaml": "2.9.0"
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
usage() {
|
|
5
|
+
echo 'usage: download-optional-run-artifacts.sh (--name <name> | --pattern <pattern>) --dir <directory>' >&2
|
|
6
|
+
exit 2
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
selector=''
|
|
10
|
+
selector_type=''
|
|
11
|
+
destination=''
|
|
12
|
+
while [ "$#" -gt 0 ]; do
|
|
13
|
+
case "$1" in
|
|
14
|
+
--name|--pattern)
|
|
15
|
+
[ -z "$selector_type" ] && [ "$#" -ge 2 ] || usage
|
|
16
|
+
selector_type="${1#--}"
|
|
17
|
+
selector="$2"
|
|
18
|
+
shift 2
|
|
19
|
+
;;
|
|
20
|
+
--dir)
|
|
21
|
+
[ -z "$destination" ] && [ "$#" -ge 2 ] || usage
|
|
22
|
+
destination="$2"
|
|
23
|
+
shift 2
|
|
24
|
+
;;
|
|
25
|
+
*) usage ;;
|
|
26
|
+
esac
|
|
27
|
+
done
|
|
28
|
+
|
|
29
|
+
[ -n "$selector_type" ] && [ -n "$selector" ] && [ -n "$destination" ] || usage
|
|
30
|
+
[ -n "${GITHUB_OUTPUT:-}" ] || { echo 'GITHUB_OUTPUT must be set' >&2; exit 2; }
|
|
31
|
+
[ -n "${GITHUB_REPOSITORY:-}" ] || { echo 'GITHUB_REPOSITORY must be set' >&2; exit 2; }
|
|
32
|
+
[ -n "${GITHUB_RUN_ID:-}" ] || { echo 'GITHUB_RUN_ID must be set' >&2; exit 2; }
|
|
33
|
+
github_host="${GH_HOST:-${GITHUB_SERVER_URL:-https://github.com}}"
|
|
34
|
+
github_host="${github_host#*://}"
|
|
35
|
+
github_host="${github_host%%/*}"
|
|
36
|
+
[ -n "$github_host" ] || { echo 'GitHub host must be set' >&2; exit 2; }
|
|
37
|
+
repository="$GITHUB_REPOSITORY"
|
|
38
|
+
case "$github_host" in
|
|
39
|
+
github.com|*.ghe.com) ;;
|
|
40
|
+
*)
|
|
41
|
+
if [ -z "${GH_ENTERPRISE_TOKEN:-${GITHUB_ENTERPRISE_TOKEN:-}}" ]; then
|
|
42
|
+
enterprise_token="${GH_TOKEN:-${GITHUB_TOKEN:-}}"
|
|
43
|
+
[ -z "$enterprise_token" ] || export GH_ENTERPRISE_TOKEN="$enterprise_token"
|
|
44
|
+
fi
|
|
45
|
+
;;
|
|
46
|
+
esac
|
|
47
|
+
|
|
48
|
+
validate_artifact_name() {
|
|
49
|
+
case "$1" in
|
|
50
|
+
''|.|..|*/*|*\\*)
|
|
51
|
+
echo '[optional-run-artifacts] invalid artifact name' >&2
|
|
52
|
+
return 2
|
|
53
|
+
;;
|
|
54
|
+
esac
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
download_exact() {
|
|
58
|
+
artifact="$1"
|
|
59
|
+
directory="$2"
|
|
60
|
+
validate_artifact_name "$artifact" || return $?
|
|
61
|
+
artifacts=$(list_artifact_names) || return $?
|
|
62
|
+
found=0
|
|
63
|
+
while IFS= read -r candidate; do
|
|
64
|
+
if [ "$candidate" = "$artifact" ]; then
|
|
65
|
+
found=1
|
|
66
|
+
break
|
|
67
|
+
fi
|
|
68
|
+
done < <(printf '%s\n' "$artifacts")
|
|
69
|
+
[ "$found" -eq 1 ] || return 3
|
|
70
|
+
echo "[optional-run-artifacts] attempt selector=$selector_type" >&2
|
|
71
|
+
GH_HOST="$github_host" gh run download "$GITHUB_RUN_ID" --repo "$repository" --name "$artifact" --dir "$directory"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
list_artifact_names() {
|
|
75
|
+
GH_HOST="$github_host" gh api \
|
|
76
|
+
--paginate \
|
|
77
|
+
"repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/artifacts?per_page=100" \
|
|
78
|
+
--jq '.artifacts[] | select(.expired | not) | .name'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
download_pattern() {
|
|
82
|
+
artifacts=$(list_artifact_names | awk '!seen[$0]++') || return $?
|
|
83
|
+
selected_artifacts=()
|
|
84
|
+
while IFS= read -r artifact; do
|
|
85
|
+
[ -n "$artifact" ] || continue
|
|
86
|
+
# shellcheck disable=SC2254
|
|
87
|
+
# selector is the caller's intended glob pattern.
|
|
88
|
+
case "$artifact" in
|
|
89
|
+
$selector) ;;
|
|
90
|
+
*) continue ;;
|
|
91
|
+
esac
|
|
92
|
+
validate_artifact_name "$artifact" || return $?
|
|
93
|
+
selected_artifacts+=("$artifact")
|
|
94
|
+
echo "[optional-run-artifacts] selected artifact=$artifact" >&2
|
|
95
|
+
done < <(printf '%s\n' "$artifacts")
|
|
96
|
+
count=${#selected_artifacts[@]}
|
|
97
|
+
echo "[optional-run-artifacts] selection selector=pattern count=$count" >&2
|
|
98
|
+
[ "$count" -gt 0 ] || return 3
|
|
99
|
+
for artifact in "${selected_artifacts[@]}"; do
|
|
100
|
+
echo "[optional-run-artifacts] attempt artifact=$artifact" >&2
|
|
101
|
+
GH_HOST="$github_host" gh run download "$GITHUB_RUN_ID" --repo "$repository" --name "$artifact" --dir "$destination/$artifact" || return $?
|
|
102
|
+
done
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if [ "$selector_type" = name ]; then
|
|
106
|
+
if download_exact "$selector" "$destination"; then status=0; else status=$?; fi
|
|
107
|
+
else
|
|
108
|
+
if download_pattern; then status=0; else status=$?; fi
|
|
109
|
+
fi
|
|
110
|
+
if [ "$status" -eq 0 ]; then
|
|
111
|
+
echo 'availability=available' >> "$GITHUB_OUTPUT"
|
|
112
|
+
echo "[optional-run-artifacts] result=available selector=$selector_type" >&2
|
|
113
|
+
else
|
|
114
|
+
case "$status" in
|
|
115
|
+
130|143) exit "$status" ;;
|
|
116
|
+
3)
|
|
117
|
+
echo 'availability=unavailable' >> "$GITHUB_OUTPUT"
|
|
118
|
+
echo "::warning::Optional same-run artifact unavailable; continuing with validated fallback (selector=$selector_type exit=$status)" >&2
|
|
119
|
+
echo "[optional-run-artifacts] result=unavailable selector=$selector_type exit=$status" >&2
|
|
120
|
+
;;
|
|
121
|
+
*)
|
|
122
|
+
echo "[optional-run-artifacts] result=error selector=$selector_type exit=$status" >&2
|
|
123
|
+
exit "$status"
|
|
124
|
+
;;
|
|
125
|
+
esac
|
|
126
|
+
fi
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
4
|
usage() {
|
|
5
|
-
echo "usage: install-github-release.sh --repo owner/name --version VERSION --asset TEMPLATE --bin NAME [--tag-prefix PREFIX] [--strip-components N] [--bin-dir DIR] [--version-flag FLAG] [--checksums-asset NAME] [--no-checksum]" >&2
|
|
5
|
+
echo "usage: install-github-release.sh --repo owner/name --version VERSION --asset TEMPLATE --bin NAME [--tag-prefix PREFIX] [--strip-components N] [--bin-dir DIR] [--version-flag FLAG] [--checksums-asset NAME] [--expected-sha256 SHA256] [--no-checksum]" >&2
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
REPO="${REPO:-}"
|
|
@@ -14,6 +14,7 @@ STRIP_COMPONENTS="${STRIP_COMPONENTS:-1}"
|
|
|
14
14
|
BIN_DIR="${INSTALL_BIN_DIR:-}"
|
|
15
15
|
VERSION_FLAG="${VERSION_FLAG:---version}"
|
|
16
16
|
CHECKSUMS_ASSET="${CHECKSUMS_ASSET:-}"
|
|
17
|
+
EXPECTED_SHA256="${EXPECTED_SHA256:-}"
|
|
17
18
|
NO_CHECKSUM="${NO_CHECKSUM:-0}"
|
|
18
19
|
|
|
19
20
|
if [ -n "${RELEASE_OWNER:-}" ] && [ -n "${RELEASE_REPO:-}" ]; then
|
|
@@ -67,6 +68,11 @@ while [ "${#}" -gt 0 ]; do
|
|
|
67
68
|
CHECKSUMS_ASSET="$2"
|
|
68
69
|
shift 2
|
|
69
70
|
;;
|
|
71
|
+
--expected-sha256)
|
|
72
|
+
[ "${#}" -ge 2 ] || { usage; exit 2; }
|
|
73
|
+
EXPECTED_SHA256="$2"
|
|
74
|
+
shift 2
|
|
75
|
+
;;
|
|
70
76
|
--no-checksum)
|
|
71
77
|
NO_CHECKSUM=1
|
|
72
78
|
shift
|
|
@@ -97,6 +103,11 @@ if ! [[ "$STRIP_COMPONENTS" =~ ^[0-9]+$ ]]; then
|
|
|
97
103
|
exit 2
|
|
98
104
|
fi
|
|
99
105
|
|
|
106
|
+
if [ -n "$EXPECTED_SHA256" ] && ! [[ "$EXPECTED_SHA256" =~ ^[[:xdigit:]]{64}$ ]]; then
|
|
107
|
+
echo "install-github-release.sh: --expected-sha256 must be a 64-character hexadecimal SHA-256 digest" >&2
|
|
108
|
+
exit 2
|
|
109
|
+
fi
|
|
110
|
+
|
|
100
111
|
BIN_DIR="${BIN_DIR:-${RUNNER_TEMP:-${HOME}/.local}/bin}"
|
|
101
112
|
mkdir -p "$BIN_DIR"
|
|
102
113
|
if [ -n "${GITHUB_PATH:-}" ]; then
|
|
@@ -150,10 +161,12 @@ extract_dir="${work}-extract"
|
|
|
150
161
|
rm -rf "$extract_dir"
|
|
151
162
|
mkdir -p "$extract_dir"
|
|
152
163
|
curl -fsSL -o "$archive" "${base_url}/${asset_name}"
|
|
153
|
-
if [ "$NO_CHECKSUM" != 1 ]; then
|
|
154
|
-
if [ -n "$
|
|
164
|
+
if [ -n "$EXPECTED_SHA256" ] || [ "$NO_CHECKSUM" != 1 ]; then
|
|
165
|
+
if [ -n "$EXPECTED_SHA256" ]; then
|
|
166
|
+
expected="$EXPECTED_SHA256"
|
|
167
|
+
elif [ -n "$checksums_name" ]; then
|
|
155
168
|
curl -fsSL -o "${archive}.checksums" "${base_url}/${checksums_name}"
|
|
156
|
-
expected=$(
|
|
169
|
+
expected=$(awk -v file="$asset_name" '$2 == file || $2 == "*" file { print $1 }' "${archive}.checksums")
|
|
157
170
|
rm "${archive}.checksums"
|
|
158
171
|
else
|
|
159
172
|
curl -fsSL -o "${archive}.sha256" "${base_url}/${asset_name}.sha256"
|