diff --git a/test/docsync-command-grammar.test.ts b/test/docsync-command-grammar.test.ts index 05a86d3f1..f43f30cda 100644 --- a/test/docsync-command-grammar.test.ts +++ b/test/docsync-command-grammar.test.ts @@ -45,6 +45,23 @@ test('captured parent tree read is accepted without granting the captured child expect(docsToolFailures(remote, fixture, [], true)).toEqual(['command outside declared docs observation interface']); }); +// Run 37166586458 slice 2 (dfaf154): the parent verified the prompt's claim that +// .qa-state/ is Git-excluded. check-ignore is a declared Git read; composition is not. +test('captured check-ignore read is declared, executes without changing the repository', () => { + const command = 'git check-ignore -v .qa-state'; + expect(docsNativeInterface(fixture)).toContain('ls-files, check-ignore, rev-parse'); + expect(docsCommandAllowed(command, fixture)).toBe(true); + const before = repoSnapshot(fixture.repo); + const result = spawnSync('bash', ['-c', command], { cwd: fixture.repo, encoding: 'utf8', timeout: 10000 }); + expect(result.status).toBe(0); + expect(result.stdout).toContain('.qa-state'); + expect(docsToolFailures(nativeResult(command, result.stdout), fixture, [], true)).toEqual([]); + expect(repoSnapshot(fixture.repo)).toEqual(before); + for (const denied of ['git check-ignore -v .qa-state; ls', 'git check-ignore -v .qa-state > out.txt']) { + expect(docsCommandAllowed(denied, fixture)).toBe(false); + } +}); + test('permitted peel reads execute as single literal Bash arguments without changing the repository', () => { const before = repoSnapshot(fixture.repo); for (const revision of ['HEAD^{tree}', 'HEAD^{}', 'HEAD^{commit}', 'HEAD^{object}', 'HEAD@{0}']) { diff --git a/test/helpers/docsync-observer.ts b/test/helpers/docsync-observer.ts index 66301cc3c..6f3ac1040 100644 --- a/test/helpers/docsync-observer.ts +++ b/test/helpers/docsync-observer.ts @@ -190,7 +190,7 @@ Keep artifacts concise: update the invocation record in place with ids, counts, } const DOCS_READ_COMMANDS = ['pwd', 'ls', 'cat', 'sha256sum', 'stat']; -const DOCS_GIT_READS = ['status', 'diff', 'show', 'log', 'ls-files', 'rev-parse', 'merge-base', 'hash-object']; +const DOCS_GIT_READS = ['status', 'diff', 'show', 'log', 'ls-files', 'check-ignore', 'rev-parse', 'merge-base', 'hash-object']; const DOCS_SAFE_GIT_CONFIG = /^(?:core\.pager=cat|core\.quotepath=(?:true|false|on|off)|color\.[a-z.]+=(?:true|false|never|always|auto))$/i; /** @@ -268,7 +268,7 @@ function literalDocsCommandAllowed(text: string, fixture: ReturnType, 'home' | 'repo' | 'skills'>, scripts: string[] = [], transport = false, insert = false): string { const skills = fixture.skills.split(path.sep).join('/'); - return `Fixture observation interface (applies to parent and every child; include this interface in child prompts): Bash may execute only separate literal pwd, ls, cat, stat, sha256sum, Git read commands (status, diff, show, log, ls-files, rev-parse, merge-base, hash-object without -w, branch --show-current), the exact generated Preamble block with its spawned prefix, or literal installed gstack-skill-start/gstack-skill-end commands for document-release (start requires GSTACK_SESSION_KIND=spawned). No shell composition, custom interpreters, arbitrary scripts, inline eval or memory-mapped writes. The only additional scripts are ${scripts.length ? scripts.join(', ') : 'none'}.${insert ? ` To insert one saved private Markdown artifact into another (a saved section file into a report), Bash may also run the single literal command cat SOURCE.md >> TARGET.md with absolute paths under ${fixture.home}.` : ''} Read/Glob/Grep remain available; Read skill and section files with Read (offset/limit for ranges), because Bash output over 30KB becomes a preview that no permitted Bash command can page. Use Write/Edit for permitted docs and private JSON/Markdown artifacts under ${fixture.home}; do not rewrite installed skills, config, actor state or scripts. No effects outside the owned fixture. The owner preserves evidence and cleans up. Missing observer coverage blocks acceptance; the Linux kernel monitor covers syscall writes in the product tree, not hostile processes or arbitrary external destinations. + return `Fixture observation interface (applies to parent and every child; include this interface in child prompts): Bash may execute only separate literal pwd, ls, cat, stat, sha256sum, Git read commands (status, diff, show, log, ls-files, check-ignore, rev-parse, merge-base, hash-object without -w, branch --show-current), the exact generated Preamble block with its spawned prefix, or literal installed gstack-skill-start/gstack-skill-end commands for document-release (start requires GSTACK_SESSION_KIND=spawned). No shell composition, custom interpreters, arbitrary scripts, inline eval or memory-mapped writes. The only additional scripts are ${scripts.length ? scripts.join(', ') : 'none'}.${insert ? ` To insert one saved private Markdown artifact into another (a saved section file into a report), Bash may also run the single literal command cat SOURCE.md >> TARGET.md with absolute paths under ${fixture.home}.` : ''} Read/Glob/Grep remain available; Read skill and section files with Read (offset/limit for ranges), because Bash output over 30KB becomes a preview that no permitted Bash command can page. Use Write/Edit for permitted docs and private JSON/Markdown artifacts under ${fixture.home}; do not rewrite installed skills, config, actor state or scripts. No effects outside the owned fixture. The owner preserves evidence and cleans up. Missing observer coverage blocks acceptance; the Linux kernel monitor covers syscall writes in the product tree, not hostile processes or arbitrary external destinations. The working directory for parent and child Bash calls is already ${fixture.repo}. Run Git reads directly, for example: git status, git diff --cached, git merge-base main HEAD, git rev-parse HEAD. Do not use Git global options such as -C, -c, --git-dir or --work-tree, and do not prepend cd or another shell wrapper. The literal git subcommand must immediately follow git; an absolute owned repository path does not make git -C an allowed command.