fix: CI run/job ids are not phone numbers; context-recovery fixture gets the full runtime bin

pii.phone.e164 redacted the run id in the published eval report's fetch command
(--run 37235771700), breaking the command it prints. Digit-only spans in a run/job
flag, gh run argument, actions/runs/ or /job/ URL segment, or run_id/job_id key are
now exempt; the same digits elsewhere still report.

context-recovery-artifacts: the fixture's ./bin/ lacked gstack-skill-start and
gstack-context-recovery (the preamble's runtime scripts since v1.71), so the agent
spent its 10 turns hunting for them and hit error_max_turns with no final message
(run 37235771700, 11 turns). Link the whole runtime bin; diagnostic rerun passed
in 6 turns.
This commit is contained in:
garrytan committed 2026-10-04 21:42:38 +00:00
1 parent 0bddbd0947
commit c245d1bc22
4 files changed
+62 -15

No files matched your search

+24 -2
View File
@@ -320,6 +320,26 @@ export function isNumericMetadataValue(span: string, match: RegExpExecArray): bo
return NUMERIC_METADATA_KEY.test(input.slice(Math.max(lineStart, start - 80), start));
}
/**
* GitHub Actions run and job ids are bare 10-12 digit integers, so
* pii.phone.e164 reads them as phone numbers. A published eval report prints
* the run id inside the command the reader copies
* (`bun run eval:pass-rates --run 37235771700`), and redacting it breaks that
* command. Only the position decides: a run/job flag (`--run 37235771700`,
* `--job=111534615007`), a `gh run view|watch|rerun|download|cancel`
* argument, an `actions/runs/<id>` or `/job/<id>` URL segment, or a
* `run_id` / `job_id` key. The same digits anywhere else still report.
*/
const CI_ID_POSITION_BEFORE =
/(?:--(?:run|job)(?:-id)?(?:[ \t]+|[ \t]*=[ \t]*)|\bgh[ \t]+run[ \t]+(?:view|watch|rerun|download|cancel)[ \t]+|\/actions\/runs\/|\/jobs?\/|(?:run|job)_id["']?[ \t]*[:=][ \t]*["']?)$/i;
export function isCiRunIdentifier(span: string, match: RegExpExecArray): boolean {
if (!/^\d+$/.test(span)) return false;
const input = match.input ?? "";
const { start } = spanBounds(match);
const lineStart = input.lastIndexOf("\n", start - 1) + 1;
return CI_ID_POSITION_BEFORE.test(input.slice(Math.max(lineStart, start - 80), start));
}
/**
* A four-part version (MAJOR.MINOR.PATCH.BUILD: .NET assembly versions,
* gstack's own VERSION) is byte-for-byte a dotted quad, and `1.128.1.0` is a
@@ -892,14 +912,16 @@ export const PATTERNS: RedactPattern[] = [
// A digit-only UUID's hyphen groups read as national phone formatting, and
// so do a county tax-map parcel ID (see looksLikeParcelId), vector
// coordinates (looksLikeDecimalCoordinates) and seed/nonce/timestamp JSON
// values (isNumericMetadataValue).
// values (isNumericMetadataValue), and GitHub Actions run and job ids in
// their id positions (isCiRunIdentifier).
validate: (span, match) =>
!insideUuid(match) &&
span.replace(/\D/g, "").length >= 10 &&
!looksLikeCompactTimestamp(span) &&
!looksLikeParcelId(span, match) &&
!looksLikeDecimalCoordinates(span) &&
!isNumericMetadataValue(span, match),
!isNumericMetadataValue(span, match) &&
!isCiRunIdentifier(span, match),
},
{
id: "pii.ssn",
+11
View File
@@ -75,3 +75,14 @@ describe('sanitizeFixedFenceLine (lines a workflow wraps in a fixed fence)', ()
expect(sanitizeFixedFenceLine('z'.repeat(10 * 1024))).toHaveLength(PUBLISHED_TEXT_MAX);
});
});
describe('id positions survive redaction', () => {
test('the eval report fetch command keeps its run id (PR #3033 comment)', () => {
const line = '✗ context-recovery-artifacts rule FAIL assertion — expect(received).toBeGreaterThanOrEqual(expected) · cause assertion Expected: >= 1 · Received: 0 [slice 10, attempt 1] evidence: paid-slice-10-a1/shards/skill-e2e-session-intelligence (fetch: bun run eval:pass-rates --run 37235771700 --case context-recovery-artifacts) after a repair: bun run scripts/test-paid-shards.ts --tier gate --case context-recovery-artifacts';
expect(sanitizeFixedFenceLine(line)).toBe(line);
expect(publishedFence([line])[1]).toBe(line);
});
test('a phone number in published text is still redacted', () => {
expect(sanitizeFixedFenceLine('call +1 415 555 0123 now')).not.toContain('415 555 0123');
});
});
+23
View File
@@ -191,3 +191,26 @@ describe("db.url_with_password: postgres:postgres only on a loopback host (#2913
],
);
});
describe("pii.phone.e164: GitHub Actions run and job ids in their id positions", () => {
table(
"pii.phone.e164",
"MEDIUM",
[
"bun run eval:pass-rates --run 37235771700 --case context-recovery-artifacts",
"bun run eval:pass-rates --run=37235771700",
"gh run view 37235771700 --job 111534615007 --log",
"gh run download 37235771700 -n paid-slice-10-a1",
"https://github.com/garrytan/gstack/actions/runs/37235771700/job/111534615007",
"GITHUB_RUN_ID=37235771700",
'{"run_id": 37235771700}',
],
[
"call 37235771700 tomorrow",
"--phone 37235771700",
"https://example.com/contact/37235771700",
'{"phone_id": 37235771700}',
"run 4155550123 laps",
],
);
});
+4 -13
View File
@@ -40,19 +40,10 @@ describeIfSelected('Session Intelligence E2E', [
run('git', ['add', '.']);
run('git', ['commit', '-m', 'initial']);
// Copy bin scripts needed by timeline and checkpoint
const binDir = path.join(workDir, 'bin');
fs.mkdirSync(binDir, { recursive: true });
for (const script of [
'gstack-timeline-log', 'gstack-timeline-read', 'gstack-slug', 'gstack-state-root.sh', 'gstack-remote-identity.sh',
'gstack-learnings-log', 'gstack-learnings-search',
]) {
const src = path.join(ROOT, 'bin', script);
if (fs.existsSync(src)) {
fs.copyFileSync(src, path.join(binDir, script));
fs.chmodSync(path.join(binDir, script), 0o755);
}
}
// The prompts send every bin call to ./bin/, and the skill preambles run
// gstack-skill-start and gstack-context-recovery, which call further bin
// scripts, so ./bin/ is the whole runtime bin directory.
fs.symlinkSync(path.join(ROOT, 'bin'), path.join(workDir, 'bin'), 'dir');
// Compute slug (same logic as gstack-slug without git remote)
slug = path.basename(workDir).replace(/[^a-zA-Z0-9._-]/g, '');