mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-06 00:44:07 +01:00
Smart App Control refuses the five unsigned binaries gstack compiles locally, and Git Bash reports it as 'Permission denied', so setup's [ -x ] gate passed and skills failed later. bin/gstack-launch-probe.sh runs each binary's side-effect-free --version and classifies it: native, blocked (spawn error, exit 126/127, or an application-control message), broken (launched, exited non-zero) or missing. It shows the raw first output line, since the exact Windows text is unverified. On Windows, setup names each blocked binary and the skills that need it, cites #2595, says which shell ran setup and whether WSL is installed, gives today's workarounds (WSL, or turning Smart App Control off), how to undo, and the new troubleshooting section. gstack-doctor's browse row now says 'blocked at launch' instead of passing, a binaries row covers the other four, and a cso row reads the build-result record. windows-setup-e2e builds with GSTACK_STRICT_BUILD=1, checks all five binaries answer --version, and adds two forced CSO failures: fatal under strict mode, and a completed setup that reports the failure and a doctor cso warning under GSTACK_STRICT_BUILD=0. No shim or launcher: it could not be called from PowerShell and would load the caller's project .env/bunfig. Signing is the follow-up in TODOS.md. Diagnosis credit: @knetics9000 (#2595, #2596), @smartjelic-sys (#2595), @pstilwell90 (#2124), @cko32002 (#2265), @salluexez (#2127). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
76 lines
3.9 KiB
TypeScript
76 lines
3.9 KiB
TypeScript
import { afterEach, describe, expect, test } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import { runBashScript } from './helpers/bash-script';
|
|
|
|
// #2595/#2124: Smart App Control blocks unsigned binaries at launch and Git
|
|
// Bash reports it as "Permission denied" (exit 126). bin/gstack-launch-probe.sh
|
|
// runs each binary's --version and tells blocked from broken from healthy.
|
|
// The exact Windows text is unverified (no SAC machine); these cases use the
|
|
// shapes reporters captured in #2595 and #2124.
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
const PROBE = path.join(ROOT, 'bin/gstack-launch-probe.sh');
|
|
const tmps: string[] = [];
|
|
afterEach(() => { for (const t of tmps.splice(0)) fs.rmSync(t, { recursive: true, force: true }); });
|
|
|
|
function fakeBinary(body: string | null, mode = 0o755) {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-launch-probe-'));
|
|
tmps.push(dir);
|
|
const file = path.join(dir, 'tool');
|
|
if (body !== null) fs.writeFileSync(file, `#!/bin/sh\n${body}\n`, { mode });
|
|
return file;
|
|
}
|
|
|
|
function probe(file: string) {
|
|
const r = runBashScript(`. ${JSON.stringify(PROBE)}\ngstack_launch_probe ${JSON.stringify(file)}\nprintf 'state=%s\\ndetail=%s\\n' "$_glp_state" "$_glp_detail"`, { timeout: 30_000 });
|
|
expect(r.status).toBe(0);
|
|
return { state: r.stdout.match(/^state=(.*)$/m)![1], detail: r.stdout.match(/^detail=(.*)$/m)![1] };
|
|
}
|
|
|
|
// POSIX execute bits stand in for a refused launch; Windows has no such bit,
|
|
// and a real SAC block cannot be produced in CI.
|
|
describe.skipIf(process.platform === 'win32')('gstack-launch-probe.sh classification', () => {
|
|
test('a binary that answers --version is native', () => {
|
|
expect(probe(fakeBinary('[ "$1" = --version ] && echo 1.2.3'))).toEqual({ state: 'native', detail: '1.2.3' });
|
|
});
|
|
|
|
test('a binary the OS refuses to execute is blocked (Git Bash "Permission denied", exit 126)', () => {
|
|
const r = probe(fakeBinary('echo never', 0o644));
|
|
expect(r.state).toBe('blocked');
|
|
expect(r.detail).toContain('Permission denied');
|
|
});
|
|
|
|
test('an application-control message is blocked even with another exit code', () => {
|
|
const r = probe(fakeBinary("echo \"Program 'browse.exe' failed to run: An Application Control policy has blocked this file\" >&2; exit 1"));
|
|
expect(r.state).toBe('blocked');
|
|
expect(r.detail).toContain('An Application Control policy has blocked this file');
|
|
});
|
|
|
|
test('a binary that launches and exits non-zero is broken, not blocked', () => {
|
|
expect(probe(fakeBinary('echo "Unknown argument: --version" >&2; exit 3'))).toEqual({ state: 'broken', detail: 'exit 3: Unknown argument: --version' });
|
|
});
|
|
|
|
test('an absent binary is missing', () => {
|
|
expect(probe(fakeBinary(null)).state).toBe('missing');
|
|
});
|
|
|
|
test('probe_all groups blocked, broken and missing binaries by repo path', () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-launch-root-'));
|
|
tmps.push(root);
|
|
const write = (rel: string, body: string, mode = 0o755) => {
|
|
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
|
fs.writeFileSync(path.join(root, rel), `#!/bin/sh\n${body}\n`, { mode });
|
|
};
|
|
write('browse/dist/browse.exe', 'echo x', 0o644);
|
|
write('browse/dist/find-browse.exe', 'echo ok');
|
|
write('design/dist/design.exe', 'echo ok');
|
|
write('make-pdf/dist/pdf.exe', 'exit 2');
|
|
const r = runBashScript(`. ${JSON.stringify(PROBE)}\ngstack_launch_probe_all ${JSON.stringify(root)} .exe\nprintf 'blocked=[%s]\\nbroken=[%s]\\nmissing=[%s]\\n' "$_glp_blocked" "$_glp_broken" "$_glp_missing"`, { timeout: 30_000 });
|
|
expect(r.stdout).toContain('blocked=[browse/dist/browse\t');
|
|
expect(r.stdout).toContain('broken=[make-pdf/dist/pdf\texit 2');
|
|
expect(r.stdout).toContain('missing=[bin/gstack-global-discover]');
|
|
expect(r.stdout).not.toContain('design/dist/design');
|
|
});
|
|
});
|