mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
160 lines
6.2 KiB
JavaScript
160 lines
6.2 KiB
JavaScript
// Pin identifier shapes from live agent transcripts so the commit-time scrub gate cannot leak account IDs.
|
||
import { readdirSync, readFileSync } from 'node:fs'
|
||
import os from 'node:os'
|
||
import { join } from 'node:path'
|
||
import { describe, expect, it } from 'vitest'
|
||
import {
|
||
formatFindings,
|
||
placeholderFor,
|
||
redactTranscript,
|
||
scanTranscriptForSecrets
|
||
} from './pty-transcript-secret-scan.mjs'
|
||
import { parseArgs, resizeMarker, resolveSpawn } from './capture-agent-pty-transcript.mjs'
|
||
|
||
describe('pty transcript secret scan', () => {
|
||
it('finds the account row of a ready screen', () => {
|
||
const findings = scanTranscriptForSecrets('Antigravity CLI 1.1.17\njin.woo@acme.dev (Business)')
|
||
expect(findings).toHaveLength(1)
|
||
expect(findings[0]).toMatchObject({ kind: 'email', line: 2, column: 1 })
|
||
})
|
||
|
||
it('finds credentials an agent may echo while signing in', () => {
|
||
const kinds = scanTranscriptForSecrets(
|
||
[
|
||
'token: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dBjftJeZ4CVP',
|
||
'key: AIzaSyA1234567890abcdefghijklmnopqrstu',
|
||
'refresh: 1//0gLm34XyZabcdefghijklmnopqrstuvwx',
|
||
'Authorization: Bearer abcdefghijklmnopqrstuvwxyz012345'
|
||
].join('\n')
|
||
).map((finding) => finding.kind)
|
||
expect(kinds).toEqual(['jwt', 'google-api-key', 'google-refresh-token', 'bearer-token'])
|
||
})
|
||
|
||
it('flags this machine’s own username, which a prompt line leaks', () => {
|
||
const username = os.userInfo().username
|
||
const findings = scanTranscriptForSecrets(`~/Users/${username}/orca/repo\n> `)
|
||
expect(findings.some((finding) => finding.kind === 'local-username')).toBe(true)
|
||
})
|
||
|
||
it('finds the resumable conversation id agy prints on exit', () => {
|
||
const findings = scanTranscriptForSecrets(
|
||
'Resume with -c (or command below):\nagy --conversation=26dc1986-9eec-456a-a534-d93e5c1076c2'
|
||
)
|
||
expect(findings).toHaveLength(1)
|
||
expect(findings[0].kind).toBe('uuid')
|
||
expect(placeholderFor('uuid', findings[0].match.length)).toMatch(
|
||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
||
)
|
||
})
|
||
|
||
it('reports a clean transcript as clean', () => {
|
||
const findings = scanTranscriptForSecrets('Antigravity CLI 1.1.17\nSonnet 4.6 (High)\n> ')
|
||
expect(findings).toEqual([])
|
||
expect(formatFindings('fixture', findings)).toContain('clean')
|
||
})
|
||
|
||
it('claims a span once, so a token inside an email is not double-reported', () => {
|
||
const email = 'longlivedaccountnamefortycharacterslongenough@corp.internal'
|
||
const findings = scanTranscriptForSecrets(email)
|
||
expect(findings).toMatchObject([{ kind: 'email', match: email, index: 0 }])
|
||
})
|
||
|
||
it('passes a fixture that is already scrubbed, so "prove it is gone" can succeed', () => {
|
||
const scrubbed = `uuuu@example.com\n${'X'.repeat(44)}`
|
||
expect(scanTranscriptForSecrets(scrubbed)).toEqual([])
|
||
})
|
||
})
|
||
|
||
describe('redaction', () => {
|
||
it('replaces every finding with the same number of characters', () => {
|
||
// Same-length redaction preserves captured wrapping; shorter replacements invalidate evidence.
|
||
const text = 'Antigravity CLI 1.1.17\njin.woo@acme.dev (Antigravity Business)\n> '
|
||
const { text: redacted, redacted: count } = redactTranscript(text)
|
||
expect(count).toBe(1)
|
||
expect(redacted).toHaveLength(text.length)
|
||
expect(redacted).not.toContain('jin.woo@acme.dev')
|
||
expect(scanTranscriptForSecrets(redacted)).toEqual([])
|
||
expect(redactTranscript(redacted).redacted).toBe(0)
|
||
})
|
||
|
||
it('keeps a redacted email shaped like an email', () => {
|
||
expect(placeholderFor('email', 'a@b.example.com'.length)).toMatch(/^u+@example\.com$/)
|
||
})
|
||
|
||
it('leaves the rest of the screen byte-for-byte untouched', () => {
|
||
const text = 'line one\nuser@corp.io\nline three'
|
||
expect(redactTranscript(text).text).toBe('line one\nXXXXXXXXXXXX\nline three')
|
||
})
|
||
})
|
||
|
||
describe('committed transcripts', () => {
|
||
// Enforce in CI: recorder warnings can be skipped, while committed transcripts persist.
|
||
const fixtureDir = join(import.meta.dirname, '..', '..', 'src', 'main', 'runtime', '__fixtures__')
|
||
const transcripts = readdirSync(fixtureDir).filter((entry) => entry.endsWith('.txt'))
|
||
|
||
it.each(transcripts)('%s carries no account identifier or credential', (name) => {
|
||
const findings = scanTranscriptForSecrets(readFileSync(join(fixtureDir, name), 'utf8'))
|
||
expect(findings, formatFindings(name, findings)).toEqual([])
|
||
})
|
||
})
|
||
|
||
describe('capture argv', () => {
|
||
it('splits recorder options from the agent command', () => {
|
||
const { options, command } = parseArgs([
|
||
'--name',
|
||
'antigravity-ready-personal-non-gemini',
|
||
'--cols',
|
||
'120',
|
||
'--',
|
||
'agy',
|
||
'--model',
|
||
'sonnet'
|
||
])
|
||
expect(options.name).toBe('antigravity-ready-personal-non-gemini')
|
||
expect(options.cols).toBe(120)
|
||
expect(command).toEqual(['agy', '--model', 'sonnet'])
|
||
})
|
||
|
||
it('collects a multi-file scan list', () => {
|
||
const { options } = parseArgs(['--scan', 'a.txt', 'b.txt', '--redact'])
|
||
expect(options.scan).toEqual(['a.txt', 'b.txt'])
|
||
expect(options.redact).toBe(true)
|
||
})
|
||
|
||
it('collects repeatable mid-capture resizes', () => {
|
||
const { options } = parseArgs([
|
||
'--name',
|
||
'codex-conpty-resize',
|
||
'--resize-at',
|
||
'3000:80x24',
|
||
'--resize-at',
|
||
'6000:140x40',
|
||
'--',
|
||
'codex'
|
||
])
|
||
expect(options.resizes).toEqual([
|
||
{ atMs: 3000, cols: 80, rows: 24 },
|
||
{ atMs: 6000, cols: 140, rows: 40 }
|
||
])
|
||
})
|
||
|
||
it('rejects a malformed resize', () => {
|
||
expect(() => parseArgs(['--resize-at', '3000:80'])).toThrow(/--resize-at expects/)
|
||
expect(() => parseArgs(['--resize-at', '3000:0x24'])).toThrow(/--resize-at expects/)
|
||
})
|
||
|
||
it('marks a resize with XTWINOPS rows;cols, which the secret scan ignores', () => {
|
||
const marker = resizeMarker(80, 24)
|
||
expect(marker).toBe(`${String.fromCharCode(27)}[8;24;80t`)
|
||
expect(scanTranscriptForSecrets(`before${marker}after`)).toEqual([])
|
||
})
|
||
|
||
it('routes a Windows shim through cmd.exe, which node-pty cannot spawn directly', () => {
|
||
expect(resolveSpawn(['agy.cmd', '--model', 'sonnet'])).toEqual(
|
||
process.platform === 'win32'
|
||
? { file: 'cmd.exe', args: ['/c', '"agy.cmd"', '--model', 'sonnet'] }
|
||
: { file: 'agy.cmd', args: ['--model', 'sonnet'] }
|
||
)
|
||
})
|
||
})
|