fix: harden browser provider activation

This commit is contained in:
Sinabina
2026-07-21 12:13:33 -07:00
parent e3effb3fc4
commit a84a6e233d
65 changed files with 1203 additions and 171 deletions
+4 -4
View File
@@ -70,11 +70,11 @@ RUN curl --retry 5 --retry-delay 5 --retry-connrefused -fsSL https://bun.sh/inst
RUN npm i -g @anthropic-ai/claude-code RUN npm i -g @anthropic-ai/claude-code
# Playwright system deps (Chromium) — needed for browse E2E tests # Playwright system deps (Chromium) — needed for browse E2E tests
RUN npx playwright install-deps chromium RUN npx playwright-core install-deps chromium
# Linux has neither Helvetica nor Arial. make-pdf's print CSS stacks fall back # Linux has neither Helvetica nor Arial. make-pdf's print CSS stacks fall back
# to Liberation Sans (metric-compatible Arial clone, SIL OFL 1.1) so PDFs don't # to Liberation Sans (metric-compatible Arial clone, SIL OFL 1.1) so PDFs don't
# render in DejaVu Sans. playwright install-deps happens to pull this in today, # render in DejaVu Sans. playwright-core install-deps happens to pull this in today,
# but the dep is implicit and could change — install explicitly so upgrades # but the dep is implicit and could change — install explicitly so upgrades
# can't silently regress rendering. # can't silently regress rendering.
# #
@@ -100,12 +100,12 @@ RUN bun install --frozen-lockfile && rm -rf /tmp/*
# Install Playwright Chromium to a shared location accessible by all users # Install Playwright Chromium to a shared location accessible by all users
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers
RUN npx playwright install chromium \ RUN npx playwright-core install chromium \
&& chmod -R a+rX /opt/playwright-browsers && chmod -R a+rX /opt/playwright-browsers
# Verify everything works # Verify everything works
RUN bun --version && node --version && claude --version && jq --version && gh --version \ RUN bun --version && node --version && claude --version && jq --version && gh --version \
&& npx playwright --version \ && npx playwright-core --version \
&& fc-match "Liberation Sans" | grep -qi "Liberation" \ && fc-match "Liberation Sans" | grep -qi "Liberation" \
|| (echo "ERROR: fonts-liberation not installed — make-pdf PDFs will render in DejaVu Sans" && exit 1) || (echo "ERROR: fonts-liberation not installed — make-pdf PDFs will render in DejaVu Sans" && exit 1)
+1 -1
View File
@@ -66,7 +66,7 @@ jobs:
fc-match -f '%{family[0]}\t%{color}\n' ':lang=und-zsye:charset=1F600' || true fc-match -f '%{family[0]}\t%{color}\n' ':lang=und-zsye:charset=1F600' || true
- name: Install Playwright Chromium - name: Install Playwright Chromium
run: bunx playwright install chromium run: bunx playwright-core install chromium
- name: Build binaries - name: Build binaries
run: bun run build run: bun run build
+2
View File
@@ -382,6 +382,7 @@ export class BrowserManager {
// BROWSE_EXTENSIONS_DIR points to an unpacked Chrome extension directory. // BROWSE_EXTENSIONS_DIR points to an unpacked Chrome extension directory.
// Extensions only work in headed mode, so we use an off-screen window. // Extensions only work in headed mode, so we use an off-screen window.
const extensionsDir = process.env.BROWSE_EXTENSIONS_DIR; const extensionsDir = process.env.BROWSE_EXTENSIONS_DIR;
if (extensionsDir) assertHeadedBrowserProvider();
const { STEALTH_LAUNCH_ARGS, buildGStackLaunchArgs } = await import('./stealth'); const { STEALTH_LAUNCH_ARGS, buildGStackLaunchArgs } = await import('./stealth');
const launchArgs: string[] = [...STEALTH_LAUNCH_ARGS, ...buildGStackLaunchArgs()]; const launchArgs: string[] = [...STEALTH_LAUNCH_ARGS, ...buildGStackLaunchArgs()];
let useHeadless = true; let useHeadless = true;
@@ -1587,6 +1588,7 @@ export class BrowserManager {
* If step 2 fails → return error, headless browser untouched * If step 2 fails → return error, headless browser untouched
*/ */
async handoff(message: string): Promise<string> { async handoff(message: string): Promise<string> {
assertHeadedBrowserProvider();
if (this.connectionMode === 'headed' || this.isHeaded) { if (this.connectionMode === 'headed' || this.isHeaded) {
return `HANDOFF: Already in headed mode at ${this.getCurrentUrl()}`; return `HANDOFF: Already in headed mode at ${this.getCurrentUrl()}`;
} }
+10 -5
View File
@@ -118,7 +118,7 @@ interface ServerState {
serverPath: string; serverPath: string;
binaryVersion?: string; binaryVersion?: string;
mode?: 'launched' | 'headed'; mode?: 'launched' | 'headed';
/** Hash of (proxyUrl + headed flag), used by D2 daemon-mismatch check. */ /** Hash of proxy, headed mode, and browser-provider intent, used by daemon-mismatch checks. */
configHash?: string; configHash?: string;
/** Xvfb child PID for cleanup on disconnect. */ /** Xvfb child PID for cleanup on disconnect. */
xvfbPid?: number; xvfbPid?: number;
@@ -431,8 +431,8 @@ async function ensureServer(flags?: GlobalFlags): Promise<ServerState> {
// hint. No silent restart — that would drop tab state, cookies, and // hint. No silent restart — that would drop tab state, cookies, and
// logged-in sessions without warning. // logged-in sessions without warning.
if (desiredHash && state.configHash && state.configHash !== desiredHash) { if (desiredHash && state.configHash && state.configHash !== desiredHash) {
console.error(`[browse] existing daemon has different config (proxy/headed mismatch).`); console.error(`[browse] existing daemon has different config (browser provider, proxy, or headed mode).`);
console.error(`[browse] run 'browse disconnect' first to apply --proxy/--headed.`); console.error(`[browse] run 'browse disconnect' first to apply the selected browser configuration.`);
process.exit(1); process.exit(1);
} }
// Same path: existing daemon is plain (no flags) but caller passes // Same path: existing daemon is plain (no flags) but caller passes
@@ -782,7 +782,7 @@ export interface GlobalFlags {
proxyUrl: string | null; proxyUrl: string | null;
/** Whether --headed was passed. */ /** Whether --headed was passed. */
headed: boolean; headed: boolean;
/** Hash of (proxy + headed) for daemon-mismatch check. */ /** Hash of proxy, headed mode, and browser-provider intent for daemon-mismatch checks. */
configHash: string; configHash: string;
/** Redacted form of proxyUrl, safe for logs. */ /** Redacted form of proxyUrl, safe for logs. */
redactedProxyUrl: string; redactedProxyUrl: string;
@@ -842,7 +842,12 @@ export function extractGlobalFlags(rawArgs: string[], env: NodeJS.ProcessEnv): G
args: out, args: out,
proxyUrl: canonicalProxyUrl, proxyUrl: canonicalProxyUrl,
headed, headed,
configHash: computeConfigHash({ proxyUrl: canonicalProxyUrl, headed }), configHash: computeConfigHash({
proxyUrl: canonicalProxyUrl,
headed,
browserProvider: env.GSTACK_BROWSER_PROVIDER,
browserExecutable: env.GSTACK_CHROMIUM_PATH,
}),
redactedProxyUrl: redactProxyUrl(canonicalProxyUrl), redactedProxyUrl: redactProxyUrl(canonicalProxyUrl),
}; };
} }
+11 -2
View File
@@ -125,7 +125,7 @@ export function toUpstreamConfig(cfg: ParsedProxyConfig): UpstreamConfig {
} }
/** /**
* Compute a stable hash of (proxyUrl + headed flag) for daemon-mismatch * Compute a stable hash of proxy, headed mode, and browser-provider intent for daemon-mismatch
* detection (D2). The hash is deterministic across CLI invocations on the * detection (D2). The hash is deterministic across CLI invocations on the
* same machine and survives daemon restarts via the state file. * same machine and survives daemon restarts via the state file.
* *
@@ -135,9 +135,18 @@ export function toUpstreamConfig(cfg: ParsedProxyConfig): UpstreamConfig {
export function computeConfigHash(opts: { export function computeConfigHash(opts: {
proxyUrl: string | null | undefined; proxyUrl: string | null | undefined;
headed: boolean; headed: boolean;
browserProvider?: string | null;
browserExecutable?: string | null;
}): string { }): string {
const proxyKey = canonicalizeProxyUrl(opts.proxyUrl); const proxyKey = canonicalizeProxyUrl(opts.proxyUrl);
const input = JSON.stringify({ proxy: proxyKey, headed: opts.headed }); const browserProvider = opts.browserProvider || null;
const browserExecutable = browserProvider === "installed" ? opts.browserExecutable || null : null;
const input = JSON.stringify({
proxy: proxyKey,
headed: opts.headed,
browserProvider,
browserExecutable,
});
return createHash('sha256').update(input).digest('hex').slice(0, 16); return createHash('sha256').update(input).digest('hex').slice(0, 16);
} }
+12 -5
View File
@@ -355,11 +355,18 @@ export async function handleWriteCommand(
} }
} catch (err: any) { } catch (err: any) {
// Enhanced error guidance: clicking <option> elements always fails (not visible / timeout) // Enhanced error guidance: clicking <option> elements always fails (not visible / timeout)
const isOption = 'locator' in resolved // Do not start a second auto-wait after the click has already timed out.
? await resolved.locator.evaluate(el => el.tagName === 'OPTION').catch(() => false) // Missing selectors used to spend 5s in click(), then block again in
: await target.locator(resolved.selector).evaluate( // evaluate() until the outer client killed the command. count() is an
el => el.tagName === 'OPTION' // immediate query and keeps the helpful option guidance only when one
).catch(() => false); // unique element actually exists.
const optionLocator = 'locator' in resolved
? resolved.locator
: target.locator(resolved.selector);
const optionCount = await optionLocator.count().catch(() => 0);
const isOption = optionCount === 1
? await optionLocator.evaluate(el => el.tagName === 'OPTION').catch(() => false)
: false;
if (isOption) { if (isOption) {
throw new Error( throw new Error(
`Cannot click <option> elements. Use 'browse select <parent-select> <value>' instead of 'click' for dropdown options.` `Cannot click <option> elements. Use 'browse select <parent-select> <value>' instead of 'click' for dropdown options.`
+14
View File
@@ -478,6 +478,20 @@ describe('Interaction', () => {
} }
}, 15000); }, 15000);
test('click on a missing selector does not start a second locator wait', async () => {
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
const started = performance.now();
try {
await handleWriteCommand('click', ['#definitely-missing-regression-node'], bm);
expect(true).toBe(false); // Should not reach here
} catch (err: any) {
expect(err.message).toContain('#definitely-missing-regression-node');
}
// click() intentionally retains Playwright's 5s auto-wait. The regression
// was a second default locator wait that pushed the total beyond 8s.
expect(performance.now() - started).toBeLessThan(6500);
}, 8000);
test('hover works', async () => { test('hover works', async () => {
const result = await handleWriteCommand('hover', ['h1'], bm); const result = await handleWriteCommand('hover', ['h1'], bm);
expect(result).toContain('Hovered'); expect(result).toContain('Hovered');
@@ -92,6 +92,47 @@ describe('D2 daemon-mismatch refuse (CLI integration)', () => {
} }
}, 15000); }, 15000);
test('refuses to reuse a same-version daemon from a different browser provider', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-provider-mismatch-'));
const stateFile = path.join(tmpDir, 'browse.json');
const fakeServer = await startFakeHealthServer('fake-token');
const { computeConfigHash } = await import('../src/proxy-config');
const managedHash = computeConfigHash({
proxyUrl: null,
headed: false,
browserProvider: 'managed',
});
fs.writeFileSync(stateFile, JSON.stringify({
pid: process.pid,
port: fakeServer.port,
token: 'fake-token',
startedAt: new Date().toISOString(),
serverPath: '',
mode: 'launched',
configHash: managedHash,
}, null, 2));
const cliEnv: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (value !== undefined) cliEnv[key] = value;
}
cliEnv.BROWSE_STATE_FILE = stateFile;
cliEnv.GSTACK_BROWSER_PROVIDER = 'installed';
cliEnv.GSTACK_CHROMIUM_PATH = process.execPath;
try {
const result = await runCli(['status'], cliEnv);
expect(result.code).toBe(1);
expect(result.stderr).toContain('different config');
expect(result.stderr).toContain('browse disconnect');
} finally {
await fakeServer.close();
try { fs.unlinkSync(stateFile); } catch { /* ignore */ }
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}, 15000);
test('refuses when existing plain daemon meets a --proxy invocation', async () => { test('refuses when existing plain daemon meets a --proxy invocation', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-mismatch-plain-')); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-mismatch-plain-'));
const stateFile = path.join(tmpDir, 'browse.json'); const stateFile = path.join(tmpDir, 'browse.json');
+16
View File
@@ -186,4 +186,20 @@ describe('extractGlobalFlags', () => {
); );
expect(a.configHash).not.toBe(b.configHash); expect(a.configHash).not.toBe(b.configHash);
}); });
test('configHash changes with browser provider and installed executable', () => {
const managed = extractGlobalFlags(['goto', 'x'], {
GSTACK_BROWSER_PROVIDER: 'managed',
} as NodeJS.ProcessEnv);
const installedA = extractGlobalFlags(['goto', 'x'], {
GSTACK_BROWSER_PROVIDER: 'installed',
GSTACK_CHROMIUM_PATH: '/browser/a',
} as NodeJS.ProcessEnv);
const installedB = extractGlobalFlags(['goto', 'x'], {
GSTACK_BROWSER_PROVIDER: 'installed',
GSTACK_CHROMIUM_PATH: '/browser/b',
} as NodeJS.ProcessEnv);
expect(managed.configHash).not.toBe(installedA.configHash);
expect(installedA.configHash).not.toBe(installedB.configHash);
});
}); });
+15 -15
View File
@@ -83,7 +83,7 @@
"source_path": "office-hours/SKILL.md.tmpl", "source_path": "office-hours/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f", "blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f",
"normalized_render_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052", "normalized_render_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
"target": "skills/plan/references/legacy/office-hours.md", "target": "skills/plan/references/legacy/office-hours.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -1416,7 +1416,7 @@
"source_path": "design-consultation/SKILL.md.tmpl", "source_path": "design-consultation/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811", "blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811",
"normalized_render_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b", "normalized_render_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
"target": "skills/design/references/legacy/design-consultation.md", "target": "skills/design/references/legacy/design-consultation.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -1591,7 +1591,7 @@
"source_path": "design-html/SKILL.md.tmpl", "source_path": "design-html/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca", "blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca",
"normalized_render_sha256": "40682d97ac83aa9178487348d5abf176334fd439e2d12f8e5cda1f8b20cd2c30", "normalized_render_sha256": "775dfc9fdcc6b96d6e267f2b8c5e7eedcf8a1d98b764c134d7111a39f3f07301",
"target": "skills/design/references/legacy/design-html.md", "target": "skills/design/references/legacy/design-html.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -1820,7 +1820,7 @@
"source_path": "design-review/SKILL.md.tmpl", "source_path": "design-review/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41", "blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41",
"normalized_render_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b", "normalized_render_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
"target": "skills/design/references/legacy/design-review.md", "target": "skills/design/references/legacy/design-review.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2162,7 +2162,7 @@
"source_path": "qa/SKILL.md.tmpl", "source_path": "qa/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8", "blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8",
"normalized_render_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55", "normalized_render_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
"target": "skills/qa/references/legacy/qa.md", "target": "skills/qa/references/legacy/qa.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2288,7 +2288,7 @@
"source_path": "qa-only/SKILL.md.tmpl", "source_path": "qa-only/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e", "blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e",
"normalized_render_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3", "normalized_render_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
"target": "skills/qa/references/legacy/qa-only.md", "target": "skills/qa/references/legacy/qa-only.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2466,7 +2466,7 @@
"source_path": "devex-review/SKILL.md.tmpl", "source_path": "devex-review/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551", "blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551",
"normalized_render_sha256": "4a907c759b6cf4202fbacaea504b1eb601a53dd35b206109d6c5105168ade7e1", "normalized_render_sha256": "6b26b22ae5cbe9483a10ad084cd6b1e8ea32d2c01e482f75b9f8b32944287d0e",
"target": "skills/qa/references/legacy/devex-review.md", "target": "skills/qa/references/legacy/devex-review.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2565,7 +2565,7 @@
"source_path": "benchmark/SKILL.md.tmpl", "source_path": "benchmark/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e", "blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e",
"normalized_render_sha256": "05ac1b123a605201546a7e95899a5b55708ca7fbb7569c58b4d755c52b45a92d", "normalized_render_sha256": "5fd14a7da7e31c24451c26d9123fcbcc376b7875c1a723bf7b69a1474e4f1c6d",
"target": "skills/qa/references/legacy/benchmark.md", "target": "skills/qa/references/legacy/benchmark.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2631,7 +2631,7 @@
"source_path": "canary/SKILL.md.tmpl", "source_path": "canary/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c", "blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c",
"normalized_render_sha256": "89be5f218da2bd812303c87b8c177081727727e5e0d2dc74eb7a73299794d5ef", "normalized_render_sha256": "b7f753ba0b98d8c7378dc797dca5950b14ebe26565bac25b5bbaa56b8ea8e13b",
"target": "skills/qa/references/legacy/canary.md", "target": "skills/qa/references/legacy/canary.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2710,7 +2710,7 @@
"source_path": "browse/SKILL.md.tmpl", "source_path": "browse/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93", "blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93",
"normalized_render_sha256": "1b532bd904b1fa1686113e8c96b70015ea6b2e6df7319a72c299de901fe5e81b", "normalized_render_sha256": "fee4ae0bd69412a6c3b1fd6737064301240b39731695b53fa10096ba48495019",
"target": "skills/qa/references/legacy/browse.md", "target": "skills/qa/references/legacy/browse.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2798,7 +2798,7 @@
"source_path": "open-gstack-browser/SKILL.md.tmpl", "source_path": "open-gstack-browser/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b", "blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b",
"normalized_render_sha256": "e6e8271ecd89761627e6e67745750b22e64596d0e51e4a2350dccd8e2ce8ebd6", "normalized_render_sha256": "f32ab85292ae920d811f4014480c48941d9b34d3ad8141840b4fd33bfdccc7dd",
"target": "skills/qa/references/legacy/open-gstack-browser.md", "target": "skills/qa/references/legacy/open-gstack-browser.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2873,7 +2873,7 @@
"source_path": "setup-browser-cookies/SKILL.md.tmpl", "source_path": "setup-browser-cookies/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652", "blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652",
"normalized_render_sha256": "7d539b2113f8cc9bf0b8b2f6e1da3dde7028176a6f71de8f47de0a98c45663e8", "normalized_render_sha256": "9e8ee39b557d1fbd032a94f5fbe16b675bdd89fe64b1c85a9af6a2ebe54aa976",
"target": "skills/qa/references/legacy/setup-browser-cookies.md", "target": "skills/qa/references/legacy/setup-browser-cookies.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2934,7 +2934,7 @@
"source_path": "pair-agent/SKILL.md.tmpl", "source_path": "pair-agent/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c", "blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c",
"normalized_render_sha256": "256fd576911cc286ddd2510daec8f4c68501cc5534f46edc044c1908574ac64a", "normalized_render_sha256": "e75661246495412102632a49d626bc313875ef479d2c570002ec66a2ccd2757a",
"target": "skills/qa/references/legacy/pair-agent.md", "target": "skills/qa/references/legacy/pair-agent.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -4136,7 +4136,7 @@
"source_path": "land-and-deploy/SKILL.md.tmpl", "source_path": "land-and-deploy/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2", "blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2",
"normalized_render_sha256": "6920f3d97ce474b8f20c8b3e38ca9d3c03973e47af33103a60bab7c02eb867bd", "normalized_render_sha256": "405924730c4e328c1a45de26576cda686d0d7da1fc4a36e840458683e1396aa2",
"target": "skills/ship/references/legacy/land-and-deploy.md", "target": "skills/ship/references/legacy/land-and-deploy.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -5775,7 +5775,7 @@
"owner_tree": "qa", "owner_tree": "qa",
"consumer_tree": "ship", "consumer_tree": "ship",
"target": "skills/ship/references/legacy/canary.md", "target": "skills/ship/references/legacy/canary.md",
"sha256": "9fcf4fdea7d52113c8f5b5cc81c1fb36df591cf9e07bcc38c4ee106b15245997", "sha256": "155174e829ef7895707ff1fad1f130fc1c7150bad9fb3598428a2efc939c5ad6",
"disposition": "SHARED_MODULE" "disposition": "SHARED_MODULE"
}, },
{ {
+1 -1
View File
@@ -40,7 +40,7 @@ manifest contains only official GitHub Release URLs and the fixed workflow
certificate identity. certificate identity.
Browser-capable archives include the Playwright-managed Chromium directory at Browser-capable archives include the Playwright-managed Chromium directory at
`.gstack-runtime-browsers`. The builder runs `playwright install chromium` `.gstack-runtime-browsers`. The builder runs `playwright-core install chromium`
only—never `--with-deps` or `sudo`—copies physical files into the immutable only—never `--with-deps` or `sudo`—copies physical files into the immutable
slot, and launch-smokes that exact Chromium on every native release runner. slot, and launch-smokes that exact Chromium on every native release runner.
The stable capability launcher sets `PLAYWRIGHT_BROWSERS_PATH` to the active The stable capability launcher sets `PLAYWRIGHT_BROWSERS_PATH` to the active
+16 -4
View File
@@ -57,13 +57,13 @@ PR, or PR-ready claim is authorized by this status.
three retained Claude Haiku live samples are classified `REGRESSION`; they three retained Claude Haiku live samples are classified `REGRESSION`; they
are preserved as noisy supplemental evidence, never cherry-picked as a are preserved as noisy supplemental evidence, never cherry-picked as a
primary gate or represented as green. primary gate or represented as green.
- [x] The current macOS GStack 2 suite is green: 151 pass / 0 fail and 1,194 - [x] The current macOS GStack 2 suite is green: 218 pass / 0 fail and 2,229
assertions across 16 files. assertions across 20 files.
- [x] Optional host-neutral runtime implemented with canonical paths, - [x] Optional host-neutral runtime implemented with canonical paths,
repo/worktree state identity, locks, atomic writes, effect claims, repo/worktree state identity, locks, atomic writes, effect claims,
doctor/config/state/cleanup, migrations, upgrade/rollback, and uninstall. doctor/config/state/cleanup, migrations, upgrade/rollback, and uninstall.
- [x] Managed runtime installer coverage is green at 25 pass / 0 fail and 341 - [x] Managed runtime installer coverage is green at 34 pass / 0 fail. The
assertions. The deterministic clean macOS arm64 managed-bundle audit records deterministic clean macOS arm64 managed-bundle audit records
110 components, 1,829 files, 450,044,315 bytes, and 50 capability launchers. 110 components, 1,829 files, 450,044,315 bytes, and 50 capability launchers.
This is a platform-specific bundle measurement, not a universal byte count; This is a platform-specific bundle measurement, not a universal byte count;
platform-native package payloads differ. Setup installs frozen platform-native package payloads differ. Setup installs frozen
@@ -75,6 +75,18 @@ PR, or PR-ready claim is authorized by this status.
production-only install with the development SDK absent, completed a local production-only install with the development SDK absent, completed a local
browser journey and Sharp full-page screenshot, and uninstalled while browser journey and Sharp full-page screenshot, and uninstalled while
preserving state. preserving state.
- [x] Browser-backed setup now fails closed until the user explicitly chooses
GStack-managed Chromium or one detected installed Chromium executable. The
local-only options step performs no network or state mutation; the signed
preview reports exact incremental bytes before a separate install approval;
only a successful install persists the host-neutral choice. Installed-browser
mode keeps the Playwright adapter while omitting managed Chromium payloads.
Visible extension-bearing GStack Browser remains managed-only. Focused setup
UX coverage is green at 22 pass / 0 fail, including no-network refusal,
provider-aware component planning, launcher propagation, and doctor launch.
The official live bootstrap remains pending until the corresponding signed
`v2.0.0-rc.6` component release is published; deterministic local evidence
does not relabel that production gate as passed.
- [x] The current candidate additionally captures a runtime-owned Bun 1.3.14 - [x] The current candidate additionally captures a runtime-owned Bun 1.3.14
executable under `.gstack-runtime-tools`, records its path/version in the executable under `.gstack-runtime-tools`, records its path/version in the
bundle manifest, vendors the tagged license/source notices, and routes the bundle manifest, vendors the tagged license/source notices, and routes the
+3 -2
View File
@@ -10,7 +10,7 @@ pass from deterministic, offline, or filesystem-only evidence.
| Command / probe | Observed result | What it proves / does not prove | | Command / probe | Observed result | What it proves / does not prove |
|---|---|---| |---|---|---|
| Earlier focused macOS `bun test test/gstack2-*.test.ts` candidate run | **Exit 0: 136 pass / 0 fail**, 1,128 assertions across 15 files. Log: `/tmp/gstack2-test-command-candidate-final2.log`. | Historical focused checkpoint retained rather than overwritten. | | Earlier focused macOS `bun test test/gstack2-*.test.ts` candidate run | **Exit 0: 136 pass / 0 fail**, 1,128 assertions across 15 files. Log: `/tmp/gstack2-test-command-candidate-final2.log`. | Historical focused checkpoint retained rather than overwritten. |
| Current macOS `bun run test:gstack2` | **Exit 0: 151 pass / 0 fail**, 1,194 assertions across 16 files. | Current generated freshness, routing, runtime, privacy, installer, upgrade, parity, and adversarial-harness surface is green. Native and broad evidence are recorded separately; this working-tree result is not attributed to the earlier native-CI commit. | | Current macOS direct `bun test --timeout 60000 test/gstack2-*.test.ts` | **Exit 0: 218 pass / 0 fail**, 2,229 assertions across 20 files. | Current routing, runtime, privacy, installer, upgrade, parity, browser-provider setup, and adversarial-harness surface is green. The `test:gstack2` wrapper's generated-cleanliness precheck remains a commit-time gate, so this row records the direct underlying suite rather than relabeling that wrapper. |
| `bun test --timeout 30000 test/gstack2-skills.test.ts test/gstack2-skills-routing.test.ts` after regeneration | **Exit 0: 3 pass / 0 fail**, 81 assertions. | The pinned corpus/parity test and both 25-scenario structured-routing tests are green. This remains structural/fixture evidence, not specialist live execution. | | `bun test --timeout 30000 test/gstack2-skills.test.ts test/gstack2-skills-routing.test.ts` after regeneration | **Exit 0: 3 pass / 0 fail**, 81 assertions. | The pinned corpus/parity test and both 25-scenario structured-routing tests are green. This remains structural/fixture evidence, not specialist live execution. |
| `bun run scripts/gstack2/run-parity.ts`, 2026-07-17 rerun | **Exit 0: 4,681 checks passed**; 55 modules, 16 sections, 25 scenarios, 16 regressions, 78 assets. | Current source/render/provenance/contract/asset/fixture parity is green. It is deterministic parity, not live-host behavior. | | `bun run scripts/gstack2/run-parity.ts`, 2026-07-17 rerun | **Exit 0: 4,681 checks passed**; 55 modules, 16 sections, 25 scenarios, 16 regressions, 78 assets. | Current source/render/provenance/contract/asset/fixture parity is green. It is deterministic parity, not live-host behavior. |
| Earlier regenerated structural parity checkpoint | **Exit 0: 2,403 checks passed** with the then-current 55/16/25/16/45 inventory. | Historical candidate checkpoint before later thin-prelude and asset coverage; superseded by the current 4,681-check rerun. | | Earlier regenerated structural parity checkpoint | **Exit 0: 2,403 checks passed** with the then-current 55/16/25/16/45 inventory. | Historical candidate checkpoint before later thin-prelude and asset coverage; superseded by the current 4,681-check rerun. |
@@ -30,7 +30,8 @@ pass from deterministic, offline, or filesystem-only evidence.
| `gstack context smoke --url https://www.context.dev --json` | **PASS:** the official `/web/scrape/markdown` endpoint returned `ok: true` and credit metadata. | The verified key entered through protected stdin/environment worked. The temporary secrets file was mode `0600`, the isolated home was removed, and the safe output did not contain the key. Committed redacted artifact: [`evals/context-dev/live-smoke-2026-07-17.json`](../../evals/context-dev/live-smoke-2026-07-17.json). | | `gstack context smoke --url https://www.context.dev --json` | **PASS:** the official `/web/scrape/markdown` endpoint returned `ok: true` and credit metadata. | The verified key entered through protected stdin/environment worked. The temporary secrets file was mode `0600`, the isolated home was removed, and the safe output did not contain the key. Committed redacted artifact: [`evals/context-dev/live-smoke-2026-07-17.json`](../../evals/context-dev/live-smoke-2026-07-17.json). |
| Standard installer matrix | **PASS: 510/510 checks**, 18 install cases, two removal cases, `skills` CLI 1.5.19. | Project/global installs pass for seven hosts, including Kimi Code CLI through the standard `.agents/skills` path; selected-skill and opt-in compatibility-alias cases, copies, and hashes pass. This remains installer/filesystem evidence. Committed artifact: [`evals/installation/install-matrix.json`](../../evals/installation/install-matrix.json). | | Standard installer matrix | **PASS: 510/510 checks**, 18 install cases, two removal cases, `skills` CLI 1.5.19. | Project/global installs pass for seven hosts, including Kimi Code CLI through the standard `.agents/skills` path; selected-skill and opt-in compatibility-alias cases, copies, and hashes pass. This remains installer/filesystem evidence. Committed artifact: [`evals/installation/install-matrix.json`](../../evals/installation/install-matrix.json). |
| Standard Agent Skills install + actual runtime-absent Codex invocation | **PASS.** Root `--list` returned exactly six. The selected source was `time-attack/gstack/skills --skill qa`; despite `skills` 1.5.19's pre-filter display counting hidden aliases, exactly one skill (`qa`) installed byte-identically. Codex exited 0 with `gstack` absent, reported `NEEDS_SETUP` and one approval prompt, changed no files, and created no runtime/browser/external-service activity. | Closes the actual-host runtime-absent judgment gate for Codex without overstating seven-host UI coverage. Artifact: [`standard-codex-runtime-absent-2026-07-17.json`](../../evals/installation/standard-codex-runtime-absent-2026-07-17.json). | | Standard Agent Skills install + actual runtime-absent Codex invocation | **PASS.** Root `--list` returned exactly six. The selected source was `time-attack/gstack/skills --skill qa`; despite `skills` 1.5.19's pre-filter display counting hidden aliases, exactly one skill (`qa`) installed byte-identically. Codex exited 0 with `gstack` absent, reported `NEEDS_SETUP` and one approval prompt, changed no files, and created no runtime/browser/external-service activity. | Closes the actual-host runtime-absent judgment gate for Codex without overstating seven-host UI coverage. Artifact: [`standard-codex-runtime-absent-2026-07-17.json`](../../evals/installation/standard-codex-runtime-absent-2026-07-17.json). |
| `bun test test/gstack2-runtime-install.test.ts` | **Exit 0: 25 pass / 0 fail**, 341 assertions. | Managed allowlist, hashes, spaces, source/internal-link rejection, production-only frozen dependencies, deterministic exact Sharp/ngrok platform closure, native-load rollback smoke, rollback/recovery, stable launchers, wrapper neutrality, and state-preserving uninstall pass. | | `bun test --timeout 30000 test/gstack2-runtime-install.test.ts` | **Exit 0: 41 pass / 0 fail.** | Managed allowlist, hashes, spaces, source/internal-link rejection, production-only frozen dependencies, deterministic exact Sharp/ngrok platform closure, native-load rollback smoke, atomic browser-choice rollback/recovery, provider-aware stable launchers, wrapper neutrality, and state-preserving uninstall pass. |
| `bun test --timeout 30000 test/gstack2-runtime-setup-ux.test.ts` | **Exit 0: 29 pass / 0 fail.** | Explicit managed/installed/later browser selection fails closed before network, local options do not mutate, signed previews remain provider-identical across same- and cross-release capability additions, developer-source provider switches replace the exact retained set, visible extension Chromium remains managed-only, and launcher/doctor paths honor the persisted selection. This is deterministic local evidence, not native Windows or every installed Chrome-family build. |
| Deterministic clean macOS arm64 managed runtime bundle audit | **110 components, 1,829 files, 450,044,315 bytes, 50 capability launchers.** | This is a platform-specific bundle measurement, not a universal byte count; platform-native package payloads differ. Setup includes the Sharp/ngrok closure and excludes the development-only Claude Agent SDK. The Hugging Face sidecar is outside the bundle and its package is development-only, so production setup installs neither its inference runtime nor model weights; the L4 capability reports unavailable. The standard skill installer remains Markdown-only. Committed artifact: [`evals/runtime-bundle/darwin-arm64.json`](../../evals/runtime-bundle/darwin-arm64.json); reproduce with `bun run scripts/gstack2/audit-runtime-bundle.ts --output evals/runtime-bundle/darwin-arm64.json`. | | Deterministic clean macOS arm64 managed runtime bundle audit | **110 components, 1,829 files, 450,044,315 bytes, 50 capability launchers.** | This is a platform-specific bundle measurement, not a universal byte count; platform-native package payloads differ. Setup includes the Sharp/ngrok closure and excludes the development-only Claude Agent SDK. The Hugging Face sidecar is outside the bundle and its package is development-only, so production setup installs neither its inference runtime nor model weights; the L4 capability reports unavailable. The standard skill installer remains Markdown-only. Committed artifact: [`evals/runtime-bundle/darwin-arm64.json`](../../evals/runtime-bundle/darwin-arm64.json); reproduce with `bun run scripts/gstack2/audit-runtime-bundle.ts --output evals/runtime-bundle/darwin-arm64.json`. |
| Earlier declared Linux Dev Container plus `bun run test:gstack2` inside it | **Exit 0: 136 pass / 0 fail, 1,127 assertions across 15 files.** Log: `/tmp/gstack2-devcontainer-gate-candidate-final4.log`. | Historical container checkpoint retained rather than overwritten; the current 150-test container result is recorded in the native-CI row below. | | Earlier declared Linux Dev Container plus `bun run test:gstack2` inside it | **Exit 0: 136 pass / 0 fail, 1,127 assertions across 15 files.** Log: `/tmp/gstack2-devcontainer-gate-candidate-final4.log`. | Historical container checkpoint retained rather than overwritten; the current 150-test container result is recorded in the native-CI row below. |
| `scripts/gstack2/runtime-install-smoke.sh` in the clean Linux arm64 container | **Pass:** production-only frozen dependencies installed with the development Agent SDK and Hugging Face/ONNX runtime absent; the managed Anthropic SDK, Sharp, and ngrok imports passed; prebuilt capabilities rebuilt; setup/doctor/version/design/PDF passed; a local-browser journey and Sharp full-page screenshot passed; uninstall preserved state. | Proves a source copy with spaces can build and complete the managed runtime lifecycle without Git history, an executable local-model stack, or Darwin-only iOS artifacts. It is not native Windows evidence. | | `scripts/gstack2/runtime-install-smoke.sh` in the clean Linux arm64 container | **Pass:** production-only frozen dependencies installed with the development Agent SDK and Hugging Face/ONNX runtime absent; the managed Anthropic SDK, Sharp, and ngrok imports passed; prebuilt capabilities rebuilt; setup/doctor/version/design/PDF passed; a local-browser journey and Sharp full-page screenshot passed; uninstall preserved state. | Proves a source copy with spaces can build and complete the managed runtime lifecycle without Git history, an executable local-model stack, or Darwin-only iOS artifacts. It is not native Windows evidence. |
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "benchmark/SKILL.md.tmpl", "source_path": "benchmark/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e", "blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e",
"normalized_render_sha256": "05ac1b123a605201546a7e95899a5b55708ca7fbb7569c58b4d755c52b45a92d", "normalized_render_sha256": "5fd14a7da7e31c24451c26d9123fcbcc376b7875c1a723bf7b69a1474e4f1c6d",
"target": "skills/qa/references/legacy/benchmark.md", "target": "skills/qa/references/legacy/benchmark.md",
"overlays": [ "overlays": [
679 679
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "browse/SKILL.md.tmpl", "source_path": "browse/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93", "blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93",
"normalized_render_sha256": "1b532bd904b1fa1686113e8c96b70015ea6b2e6df7319a72c299de901fe5e81b", "normalized_render_sha256": "fee4ae0bd69412a6c3b1fd6737064301240b39731695b53fa10096ba48495019",
"target": "skills/qa/references/legacy/browse.md", "target": "skills/qa/references/legacy/browse.md",
"overlays": [ "overlays": [
679, 679,
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "canary/SKILL.md.tmpl", "source_path": "canary/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c", "blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c",
"normalized_render_sha256": "89be5f218da2bd812303c87b8c177081727727e5e0d2dc74eb7a73299794d5ef", "normalized_render_sha256": "b7f753ba0b98d8c7378dc797dca5950b14ebe26565bac25b5bbaa56b8ea8e13b",
"target": "skills/qa/references/legacy/canary.md", "target": "skills/qa/references/legacy/canary.md",
"overlays": [ "overlays": [
679, 679,
@@ -9,7 +9,7 @@
"source_path": "design-consultation/SKILL.md.tmpl", "source_path": "design-consultation/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811", "blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811",
"normalized_render_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b", "normalized_render_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
"target": "skills/design/references/legacy/design-consultation.md", "target": "skills/design/references/legacy/design-consultation.md",
"overlays": [ "overlays": [
679, 679,
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "design-html/SKILL.md.tmpl", "source_path": "design-html/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca", "blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca",
"normalized_render_sha256": "40682d97ac83aa9178487348d5abf176334fd439e2d12f8e5cda1f8b20cd2c30", "normalized_render_sha256": "775dfc9fdcc6b96d6e267f2b8c5e7eedcf8a1d98b764c134d7111a39f3f07301",
"target": "skills/design/references/legacy/design-html.md", "target": "skills/design/references/legacy/design-html.md",
"overlays": [ "overlays": [
679 679
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "design-review/SKILL.md.tmpl", "source_path": "design-review/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41", "blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41",
"normalized_render_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b", "normalized_render_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
"target": "skills/design/references/legacy/design-review.md", "target": "skills/design/references/legacy/design-review.md",
"overlays": [ "overlays": [
679, 679,
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "devex-review/SKILL.md.tmpl", "source_path": "devex-review/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551", "blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551",
"normalized_render_sha256": "4a907c759b6cf4202fbacaea504b1eb601a53dd35b206109d6c5105168ade7e1", "normalized_render_sha256": "6b26b22ae5cbe9483a10ad084cd6b1e8ea32d2c01e482f75b9f8b32944287d0e",
"target": "skills/qa/references/legacy/devex-review.md", "target": "skills/qa/references/legacy/devex-review.md",
"overlays": [ "overlays": [
679, 679,
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "land-and-deploy/SKILL.md.tmpl", "source_path": "land-and-deploy/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2", "blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2",
"normalized_render_sha256": "6920f3d97ce474b8f20c8b3e38ca9d3c03973e47af33103a60bab7c02eb867bd", "normalized_render_sha256": "405924730c4e328c1a45de26576cda686d0d7da1fc4a36e840458683e1396aa2",
"target": "skills/ship/references/legacy/land-and-deploy.md", "target": "skills/ship/references/legacy/land-and-deploy.md",
"overlays": [ "overlays": [
679, 679,
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "office-hours/SKILL.md.tmpl", "source_path": "office-hours/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f", "blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f",
"normalized_render_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052", "normalized_render_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
"target": "skills/plan/references/legacy/office-hours.md", "target": "skills/plan/references/legacy/office-hours.md",
"overlays": [ "overlays": [
679, 679,
@@ -9,7 +9,7 @@
"source_path": "open-gstack-browser/SKILL.md.tmpl", "source_path": "open-gstack-browser/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b", "blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b",
"normalized_render_sha256": "e6e8271ecd89761627e6e67745750b22e64596d0e51e4a2350dccd8e2ce8ebd6", "normalized_render_sha256": "f32ab85292ae920d811f4014480c48941d9b34d3ad8141840b4fd33bfdccc7dd",
"target": "skills/qa/references/legacy/open-gstack-browser.md", "target": "skills/qa/references/legacy/open-gstack-browser.md",
"overlays": [ "overlays": [
679 679
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "pair-agent/SKILL.md.tmpl", "source_path": "pair-agent/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c", "blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c",
"normalized_render_sha256": "256fd576911cc286ddd2510daec8f4c68501cc5534f46edc044c1908574ac64a", "normalized_render_sha256": "e75661246495412102632a49d626bc313875ef479d2c570002ec66a2ccd2757a",
"target": "skills/qa/references/legacy/pair-agent.md", "target": "skills/qa/references/legacy/pair-agent.md",
"overlays": [ "overlays": [
679 679
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "qa-only/SKILL.md.tmpl", "source_path": "qa-only/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e", "blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e",
"normalized_render_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3", "normalized_render_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
"target": "skills/qa/references/legacy/qa-only.md", "target": "skills/qa/references/legacy/qa-only.md",
"overlays": [ "overlays": [
679, 679,
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "qa/SKILL.md.tmpl", "source_path": "qa/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8", "blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8",
"normalized_render_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55", "normalized_render_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
"target": "skills/qa/references/legacy/qa.md", "target": "skills/qa/references/legacy/qa.md",
"overlays": [ "overlays": [
679, 679,
@@ -9,7 +9,7 @@
"source_path": "setup-browser-cookies/SKILL.md.tmpl", "source_path": "setup-browser-cookies/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652", "blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652",
"normalized_render_sha256": "7d539b2113f8cc9bf0b8b2f6e1da3dde7028176a6f71de8f47de0a98c45663e8", "normalized_render_sha256": "9e8ee39b557d1fbd032a94f5fbe16b675bdd89fe64b1c85a9af6a2ebe54aa976",
"target": "skills/qa/references/legacy/setup-browser-cookies.md", "target": "skills/qa/references/legacy/setup-browser-cookies.md",
"overlays": [ "overlays": [
679 679
+15 -15
View File
@@ -83,7 +83,7 @@
"source_path": "office-hours/SKILL.md.tmpl", "source_path": "office-hours/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f", "blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f",
"normalized_render_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052", "normalized_render_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
"target": "skills/plan/references/legacy/office-hours.md", "target": "skills/plan/references/legacy/office-hours.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -1416,7 +1416,7 @@
"source_path": "design-consultation/SKILL.md.tmpl", "source_path": "design-consultation/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811", "blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811",
"normalized_render_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b", "normalized_render_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
"target": "skills/design/references/legacy/design-consultation.md", "target": "skills/design/references/legacy/design-consultation.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -1591,7 +1591,7 @@
"source_path": "design-html/SKILL.md.tmpl", "source_path": "design-html/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca", "blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca",
"normalized_render_sha256": "40682d97ac83aa9178487348d5abf176334fd439e2d12f8e5cda1f8b20cd2c30", "normalized_render_sha256": "775dfc9fdcc6b96d6e267f2b8c5e7eedcf8a1d98b764c134d7111a39f3f07301",
"target": "skills/design/references/legacy/design-html.md", "target": "skills/design/references/legacy/design-html.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -1820,7 +1820,7 @@
"source_path": "design-review/SKILL.md.tmpl", "source_path": "design-review/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41", "blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41",
"normalized_render_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b", "normalized_render_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
"target": "skills/design/references/legacy/design-review.md", "target": "skills/design/references/legacy/design-review.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2162,7 +2162,7 @@
"source_path": "qa/SKILL.md.tmpl", "source_path": "qa/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8", "blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8",
"normalized_render_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55", "normalized_render_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
"target": "skills/qa/references/legacy/qa.md", "target": "skills/qa/references/legacy/qa.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2288,7 +2288,7 @@
"source_path": "qa-only/SKILL.md.tmpl", "source_path": "qa-only/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e", "blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e",
"normalized_render_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3", "normalized_render_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
"target": "skills/qa/references/legacy/qa-only.md", "target": "skills/qa/references/legacy/qa-only.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2466,7 +2466,7 @@
"source_path": "devex-review/SKILL.md.tmpl", "source_path": "devex-review/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551", "blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551",
"normalized_render_sha256": "4a907c759b6cf4202fbacaea504b1eb601a53dd35b206109d6c5105168ade7e1", "normalized_render_sha256": "6b26b22ae5cbe9483a10ad084cd6b1e8ea32d2c01e482f75b9f8b32944287d0e",
"target": "skills/qa/references/legacy/devex-review.md", "target": "skills/qa/references/legacy/devex-review.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2565,7 +2565,7 @@
"source_path": "benchmark/SKILL.md.tmpl", "source_path": "benchmark/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e", "blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e",
"normalized_render_sha256": "05ac1b123a605201546a7e95899a5b55708ca7fbb7569c58b4d755c52b45a92d", "normalized_render_sha256": "5fd14a7da7e31c24451c26d9123fcbcc376b7875c1a723bf7b69a1474e4f1c6d",
"target": "skills/qa/references/legacy/benchmark.md", "target": "skills/qa/references/legacy/benchmark.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2631,7 +2631,7 @@
"source_path": "canary/SKILL.md.tmpl", "source_path": "canary/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c", "blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c",
"normalized_render_sha256": "89be5f218da2bd812303c87b8c177081727727e5e0d2dc74eb7a73299794d5ef", "normalized_render_sha256": "b7f753ba0b98d8c7378dc797dca5950b14ebe26565bac25b5bbaa56b8ea8e13b",
"target": "skills/qa/references/legacy/canary.md", "target": "skills/qa/references/legacy/canary.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2710,7 +2710,7 @@
"source_path": "browse/SKILL.md.tmpl", "source_path": "browse/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93", "blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93",
"normalized_render_sha256": "1b532bd904b1fa1686113e8c96b70015ea6b2e6df7319a72c299de901fe5e81b", "normalized_render_sha256": "fee4ae0bd69412a6c3b1fd6737064301240b39731695b53fa10096ba48495019",
"target": "skills/qa/references/legacy/browse.md", "target": "skills/qa/references/legacy/browse.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2798,7 +2798,7 @@
"source_path": "open-gstack-browser/SKILL.md.tmpl", "source_path": "open-gstack-browser/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b", "blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b",
"normalized_render_sha256": "e6e8271ecd89761627e6e67745750b22e64596d0e51e4a2350dccd8e2ce8ebd6", "normalized_render_sha256": "f32ab85292ae920d811f4014480c48941d9b34d3ad8141840b4fd33bfdccc7dd",
"target": "skills/qa/references/legacy/open-gstack-browser.md", "target": "skills/qa/references/legacy/open-gstack-browser.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2873,7 +2873,7 @@
"source_path": "setup-browser-cookies/SKILL.md.tmpl", "source_path": "setup-browser-cookies/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652", "blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652",
"normalized_render_sha256": "7d539b2113f8cc9bf0b8b2f6e1da3dde7028176a6f71de8f47de0a98c45663e8", "normalized_render_sha256": "9e8ee39b557d1fbd032a94f5fbe16b675bdd89fe64b1c85a9af6a2ebe54aa976",
"target": "skills/qa/references/legacy/setup-browser-cookies.md", "target": "skills/qa/references/legacy/setup-browser-cookies.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2934,7 +2934,7 @@
"source_path": "pair-agent/SKILL.md.tmpl", "source_path": "pair-agent/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c", "blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c",
"normalized_render_sha256": "256fd576911cc286ddd2510daec8f4c68501cc5534f46edc044c1908574ac64a", "normalized_render_sha256": "e75661246495412102632a49d626bc313875ef479d2c570002ec66a2ccd2757a",
"target": "skills/qa/references/legacy/pair-agent.md", "target": "skills/qa/references/legacy/pair-agent.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -4136,7 +4136,7 @@
"source_path": "land-and-deploy/SKILL.md.tmpl", "source_path": "land-and-deploy/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2", "blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2",
"normalized_render_sha256": "6920f3d97ce474b8f20c8b3e38ca9d3c03973e47af33103a60bab7c02eb867bd", "normalized_render_sha256": "405924730c4e328c1a45de26576cda686d0d7da1fc4a36e840458683e1396aa2",
"target": "skills/ship/references/legacy/land-and-deploy.md", "target": "skills/ship/references/legacy/land-and-deploy.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -5775,7 +5775,7 @@
"owner_tree": "qa", "owner_tree": "qa",
"consumer_tree": "ship", "consumer_tree": "ship",
"target": "skills/ship/references/legacy/canary.md", "target": "skills/ship/references/legacy/canary.md",
"sha256": "9fcf4fdea7d52113c8f5b5cc81c1fb36df591cf9e07bcc38c4ee106b15245997", "sha256": "155174e829ef7895707ff1fad1f130fc1c7150bad9fb3598428a2efc939c5ad6",
"disposition": "SHARED_MODULE" "disposition": "SHARED_MODULE"
}, },
{ {
@@ -57,18 +57,18 @@
} }
}, },
"mechanical_port": { "mechanical_port": {
"rendered_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b", "rendered_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
"differs_from_baseline": true, "differs_from_baseline": true,
"allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references." "allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references."
}, },
"candidate": { "candidate": {
"target_path": "skills/design/references/legacy/design-consultation.md", "target_path": "skills/design/references/legacy/design-consultation.md",
"rendered_legacy_body_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b", "rendered_legacy_body_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
"semantic_signature": { "semantic_signature": {
"normalized_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b", "normalized_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
"headings_sha256": "78cdfd5aa0c0264964542d45190c919c7adb8c01ea68a22d67dd0fad538cc643", "headings_sha256": "78cdfd5aa0c0264964542d45190c919c7adb8c01ea68a22d67dd0fad538cc643",
"questions_sha256": "91adedef9aa8000a9aa3385381227d149dbc52cbf2d127656bf0697510b62906", "questions_sha256": "91adedef9aa8000a9aa3385381227d149dbc52cbf2d127656bf0697510b62906",
"obligations_sha256": "c36974d06c0f66d0a0f2ea3d27282ed785c7cc956cce40a8c8db35492680c972", "obligations_sha256": "fa0722c312610cfe74c70b1e29f447622b360cb894f3c697b3ca959c3a068ddb",
"heading_count": 12, "heading_count": 12,
"question_count": 2, "question_count": 2,
"obligation_count": 18 "obligation_count": 18
@@ -57,18 +57,18 @@
} }
}, },
"mechanical_port": { "mechanical_port": {
"rendered_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b", "rendered_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
"differs_from_baseline": true, "differs_from_baseline": true,
"allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references." "allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references."
}, },
"candidate": { "candidate": {
"target_path": "skills/design/references/legacy/design-review.md", "target_path": "skills/design/references/legacy/design-review.md",
"rendered_legacy_body_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b", "rendered_legacy_body_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
"semantic_signature": { "semantic_signature": {
"normalized_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b", "normalized_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
"headings_sha256": "739abb73da8446c6133835f56ea0c862a7c1fd57e6702a6afe1ed1fd2fb2b2c8", "headings_sha256": "739abb73da8446c6133835f56ea0c862a7c1fd57e6702a6afe1ed1fd2fb2b2c8",
"questions_sha256": "57579e50936d62734df12299638bdcf8ce412f24b68688b071be156312aafcbc", "questions_sha256": "57579e50936d62734df12299638bdcf8ce412f24b68688b071be156312aafcbc",
"obligations_sha256": "e4fe261ecc3956834dfac15cac60fc4b8057ad0c3b37ede06ee4667d61852655", "obligations_sha256": "3e3b3b8a8e43c0120d60f5517cc03aa64bb31bf50adbe418ba89a7ae63557a55",
"heading_count": 62, "heading_count": 62,
"question_count": 24, "question_count": 24,
"obligation_count": 63 "obligation_count": 63
@@ -58,18 +58,18 @@
} }
}, },
"mechanical_port": { "mechanical_port": {
"rendered_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052", "rendered_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
"differs_from_baseline": true, "differs_from_baseline": true,
"allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references." "allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references."
}, },
"candidate": { "candidate": {
"target_path": "skills/plan/references/legacy/office-hours.md", "target_path": "skills/plan/references/legacy/office-hours.md",
"rendered_legacy_body_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052", "rendered_legacy_body_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
"semantic_signature": { "semantic_signature": {
"normalized_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052", "normalized_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
"headings_sha256": "f50aa767e26cdfc3c8fa4c6bdcabd10e061ca0ab59e2aff4c204eaf68bfd1d56", "headings_sha256": "f50aa767e26cdfc3c8fa4c6bdcabd10e061ca0ab59e2aff4c204eaf68bfd1d56",
"questions_sha256": "92d854d47b2f92f30cc34a59cfb6f3e18449beddcb8ebd5490162545bc8384c7", "questions_sha256": "92d854d47b2f92f30cc34a59cfb6f3e18449beddcb8ebd5490162545bc8384c7",
"obligations_sha256": "c6f9f220f431b50b7721ac9baa3ae3d7be5de88b489c80a45350028c96a9db84", "obligations_sha256": "a8abcb960325c146cb555a71821d702b593bb7dc4f12646d4ecaea11a9cf6b68",
"heading_count": 34, "heading_count": 34,
"question_count": 13, "question_count": 13,
"obligation_count": 43 "obligation_count": 43
@@ -57,18 +57,18 @@
} }
}, },
"mechanical_port": { "mechanical_port": {
"rendered_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55", "rendered_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
"differs_from_baseline": true, "differs_from_baseline": true,
"allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references." "allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references."
}, },
"candidate": { "candidate": {
"target_path": "skills/qa/references/legacy/qa.md", "target_path": "skills/qa/references/legacy/qa.md",
"rendered_legacy_body_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55", "rendered_legacy_body_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
"semantic_signature": { "semantic_signature": {
"normalized_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55", "normalized_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
"headings_sha256": "dd0b56f79cee31c4c3c71f32fb0a4438f686c6ae517625a7d59475deecd4c75e", "headings_sha256": "dd0b56f79cee31c4c3c71f32fb0a4438f686c6ae517625a7d59475deecd4c75e",
"questions_sha256": "8fece376011d8a8606bcd05d52332f77b56de551b7f1abde94bff1f33ef0a9c1", "questions_sha256": "8fece376011d8a8606bcd05d52332f77b56de551b7f1abde94bff1f33ef0a9c1",
"obligations_sha256": "c9a3696553c10b1338568d728f86f47c9a7a06b2defa37bd557c662d4a9086d8", "obligations_sha256": "8c9e044c4224e4c35838d6ae9241405a5df42c2372c2ce272e0a11f37ab68c86",
"heading_count": 58, "heading_count": 58,
"question_count": 6, "question_count": 6,
"obligation_count": 65 "obligation_count": 65
@@ -57,18 +57,18 @@
} }
}, },
"mechanical_port": { "mechanical_port": {
"rendered_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3", "rendered_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
"differs_from_baseline": true, "differs_from_baseline": true,
"allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references." "allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references."
}, },
"candidate": { "candidate": {
"target_path": "skills/qa/references/legacy/qa-only.md", "target_path": "skills/qa/references/legacy/qa-only.md",
"rendered_legacy_body_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3", "rendered_legacy_body_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
"semantic_signature": { "semantic_signature": {
"normalized_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3", "normalized_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
"headings_sha256": "0241f7efa9ffcaef764bd6517099b03394e09f90c8d1f3f015c7c7a135f78201", "headings_sha256": "0241f7efa9ffcaef764bd6517099b03394e09f90c8d1f3f015c7c7a135f78201",
"questions_sha256": "8fece376011d8a8606bcd05d52332f77b56de551b7f1abde94bff1f33ef0a9c1", "questions_sha256": "8fece376011d8a8606bcd05d52332f77b56de551b7f1abde94bff1f33ef0a9c1",
"obligations_sha256": "9ea973707574aa3750e38a2920e65072f804fe7cddf789f1710e08b0a44d92d3", "obligations_sha256": "f16cce08f386155cae52758fb3f7235e1c8090ca7d84117caa4761b76927d9d5",
"heading_count": 34, "heading_count": 34,
"question_count": 6, "question_count": 6,
"obligation_count": 40 "obligation_count": 40
+3 -5
View File
@@ -627,13 +627,11 @@ async function upgradeCommand({ args, home, stdout, installOptions = {} }) {
if (parsed.positionals.length) throw cliError("Upgrade accepts only named options", "USAGE"); if (parsed.positionals.length) throw cliError("Upgrade accepts only named options", "USAGE");
if (parsed.flags.has("--rollback")) { if (parsed.flags.has("--rollback")) {
if (parsed.values.has("--source") || parsed.values.has("--version")) throw cliError("--rollback cannot be combined with staging options", "USAGE"); if (parsed.values.has("--source") || parsed.values.has("--version")) throw cliError("--rollback cannot be combined with staging options", "USAGE");
let rollbackBrowserChoice = null;
const pointer = await rollbackUpgrade(home, { const pointer = await rollbackUpgrade(home, {
healthCheck: async (fallbackPath) => { prepareActivation: async (fallbackPath) => ({
rollbackBrowserChoice = await resolvedBrowserChoiceForRuntimePath(fallbackPath); browserChoice: await resolvedBrowserChoiceForRuntimePath(fallbackPath),
}, }),
}); });
if (rollbackBrowserChoice) await configSetBrowserChoice(home, rollbackBrowserChoice);
write(stdout, parsed.flags.has("--json") ? `${JSON.stringify(pointer, null, 2)}\n` : `Rolled back to ${pointer.current}\n`); write(stdout, parsed.flags.has("--json") ? `${JSON.stringify(pointer, null, 2)}\n` : `Rolled back to ${pointer.current}\n`);
return 0; return 0;
} }
+23 -8
View File
@@ -143,7 +143,11 @@ export async function runDoctor(options = {}) {
add("specialist-tool:python", python.ok ? "pass" : "warn", python.message, python.details); add("specialist-tool:python", python.ok ? "pass" : "warn", python.message, python.details);
const selected = new Set(Array.isArray(manifest?.selectedCapabilities) ? manifest.selectedCapabilities : []); const selected = new Set(Array.isArray(manifest?.selectedCapabilities) ? manifest.selectedCapabilities : []);
const launchers = manifest?.capabilities ?? {}; const launchers = manifest?.capabilities ?? {};
for (const capability of OPTIONAL_RUNTIME_CAPABILITIES) { const capabilitiesToInspect = [
...OPTIONAL_RUNTIME_CAPABILITIES,
...(selected.has("browser-visible") ? ["browser-visible"] : []),
];
for (const capability of capabilitiesToInspect) {
if (!selected.has(capability)) { if (!selected.has(capability)) {
add(`capability:${capability}`, "warn", "not selected"); add(`capability:${capability}`, "warn", "not selected");
continue; continue;
@@ -158,8 +162,11 @@ export async function runDoctor(options = {}) {
add(`capability:${capability}`, "fail", "selected but required launcher metadata is missing"); add(`capability:${capability}`, "fail", "selected but required launcher metadata is missing");
continue; continue;
} }
if (capability === "browser") { if (capability === "browser" || capability === "browser-visible") {
const browser = runtimeConfig?.browser?.provider === "installed" const visible = capability === "browser-visible";
const browser = visible && runtimeConfig?.browser?.provider !== "managed"
? { ok: false, message: "visible GStack Browser requires the managed Chromium provider" }
: runtimeConfig?.browser?.provider === "installed"
? await inspectInstalledChromium( ? await inspectInstalledChromium(
activeRoot, activeRoot,
options.nodeCommand ?? process.env.GSTACK_NODE ?? "node", options.nodeCommand ?? process.env.GSTACK_NODE ?? "node",
@@ -167,7 +174,11 @@ export async function runDoctor(options = {}) {
options, options,
) )
: runtimeConfig?.browser?.provider === "managed" : runtimeConfig?.browser?.provider === "managed"
? await inspectManagedChromium(activeRoot, options.nodeCommand ?? process.env.GSTACK_NODE ?? "node") ? await inspectManagedChromium(
activeRoot,
options.nodeCommand ?? process.env.GSTACK_NODE ?? "node",
{ visible },
)
: { ok: false, message: "browser capability is installed, but no browser provider was explicitly selected" }; : { ok: false, message: "browser capability is installed, but no browser provider was explicitly selected" };
add(`capability:${capability}`, browser.ok ? "pass" : "fail", browser.message, browser.details); add(`capability:${capability}`, browser.ok ? "pass" : "fail", browser.message, browser.details);
continue; continue;
@@ -245,7 +256,7 @@ async function inspectPython(env) {
return { ok: false, message: "Python 3 is absent; only specialist flows that explicitly request it are unavailable" }; return { ok: false, message: "Python 3 is absent; only specialist flows that explicitly request it are unavailable" };
} }
async function inspectManagedChromium(activeRoot, nodeCommand) { async function inspectManagedChromium(activeRoot, nodeCommand, options = {}) {
const browserRoot = path.join(activeRoot, ".gstack-runtime-browsers"); const browserRoot = path.join(activeRoot, ".gstack-runtime-browsers");
const modulePath = path.join(activeRoot, "node_modules", "playwright", "index.mjs"); const modulePath = path.join(activeRoot, "node_modules", "playwright", "index.mjs");
const [browserStat, moduleStat] = await Promise.all([ const [browserStat, moduleStat] = await Promise.all([
@@ -260,11 +271,15 @@ async function inspectManagedChromium(activeRoot, nodeCommand) {
const result = await captureCommand(nodeCommand, [ const result = await captureCommand(nodeCommand, [
"--input-type=module", "--input-type=module",
"--eval", "--eval",
`const { chromium } = await import(${JSON.stringify(moduleUrl)}); const browser = await chromium.launch({ headless: true }); try { process.stdout.write(browser.version()); } finally { await browser.close(); }`, `const { chromium } = await import(${JSON.stringify(moduleUrl)}); const browser = await chromium.launch(${options.visible ? '{ headless: true, channel: "chromium" }' : "{ headless: true }"}); try { process.stdout.write(browser.version()); } finally { await browser.close(); }`,
], { env: { ...process.env, PLAYWRIGHT_BROWSERS_PATH: browserRoot } }); ], { env: { ...process.env, PLAYWRIGHT_BROWSERS_PATH: browserRoot } });
const version = result.stdout.trim(); const version = result.stdout.trim();
if (!version) return { ok: false, message: "managed Chromium launched without reporting a browser version" }; if (!version) return { ok: false, message: "managed Chromium launched without reporting a browser version" };
return { ok: true, message: `managed headless Chromium ${version} launches and exits cleanly`, details: { browserRoot, version } }; return {
ok: true,
message: `managed ${options.visible ? "visible-capable" : "headless"} Chromium ${version} launches and exits cleanly`,
details: { browserRoot, version },
};
} catch (error) { } catch (error) {
return { ok: false, message: `managed Chromium is not runnable: ${error.message}` }; return { ok: false, message: `managed Chromium is not runnable: ${error.message}` };
} }
@@ -311,7 +326,7 @@ async function inspectXcrun() {
} }
function capabilityLaunchersReady(capability, launchers) { function capabilityLaunchersReady(capability, launchers) {
if (capability === "browser") return typeof launchers.browse === "string"; if (capability === "browser" || capability === "browser-visible") return typeof launchers.browse === "string";
if (capability === "design") return typeof launchers["gstack-design"] === "string"; if (capability === "design") return typeof launchers["gstack-design"] === "string";
if (capability === "pdf") return typeof launchers["make-pdf"] === "string"; if (capability === "pdf") return typeof launchers["make-pdf"] === "string";
if (capability === "diagram") return true; if (capability === "diagram") return true;
+14 -7
View File
@@ -1641,6 +1641,12 @@ const bundle = await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8")
const browserBacked = relative.startsWith("browse/") || relative.startsWith("make-pdf/"); const browserBacked = relative.startsWith("browse/") || relative.startsWith("make-pdf/");
const selectedCapabilities = Array.isArray(bundle?.selectedCapabilities) ? bundle.selectedCapabilities : []; const selectedCapabilities = Array.isArray(bundle?.selectedCapabilities) ? bundle.selectedCapabilities : [];
const runtimeComponents = Array.isArray(bundle?.runtimeComponents) ? bundle.runtimeComponents : []; const runtimeComponents = Array.isArray(bundle?.runtimeComponents) ? bundle.runtimeComponents : [];
const visibleRequested = relative.startsWith("browse/") && (
args.includes("connect") ||
args.includes("handoff") ||
args.includes("--headed") ||
(args[0] === "pair-agent" && !args.includes("--headless"))
);
const slotProvider = bundle?.browserChoice?.provider ?? ( const slotProvider = bundle?.browserChoice?.provider ?? (
runtimeComponents.includes("browser-headless") || runtimeComponents.includes("browser-visible") runtimeComponents.includes("browser-headless") || runtimeComponents.includes("browser-visible")
? "managed" ? "managed"
@@ -1656,16 +1662,16 @@ if (browserBacked) {
if (slotProvider && browserChoice.provider !== slotProvider) { if (slotProvider && browserChoice.provider !== slotProvider) {
throw new Error("The selected browser provider does not match the active runtime slot; run the signed browser capability bootstrap for the selected provider"); throw new Error("The selected browser provider does not match the active runtime slot; run the signed browser capability bootstrap for the selected provider");
} }
if (visibleRequested && !selectedCapabilities.includes("browser-visible")) {
if (browserChoice.provider === "installed") {
throw new Error("Visible GStack Browser requires managed Chromium; preview and approve the browser-visible capability first");
}
throw new Error("The active runtime slot does not include visible Chromium; preview and approve the browser-visible capability first");
}
if (browserChoice.provider === "installed") { if (browserChoice.provider === "installed") {
if (selectedCapabilities.includes("browser-visible")) { if (selectedCapabilities.includes("browser-visible")) {
throw new Error("Visible GStack Browser requires a managed Chromium runtime slot"); throw new Error("Visible GStack Browser requires a managed Chromium runtime slot");
} }
const visibleRequested = relative.startsWith("browse/") && (
args.includes("connect") ||
args.includes("handoff") ||
args.includes("--headed") ||
(args[0] === "pair-agent" && !args.includes("--headless"))
);
if (visibleRequested) { if (visibleRequested) {
throw new Error("Visible GStack Browser requires managed Chromium; preview and approve the browser-visible capability first"); throw new Error("Visible GStack Browser requires managed Chromium; preview and approve the browser-visible capability first");
} }
@@ -1835,7 +1841,8 @@ function processIsAlive(pid) {
} }
function validTransactionPath(value) { function validTransactionPath(value) {
if (value === "runtime-install.json" || (typeof value === "string" && /^bin\\/[A-Za-z0-9._-]+$/.test(value))) return value; if (value === "config.json" || value === "runtime-install.json" ||
(typeof value === "string" && /^bin\\/[A-Za-z0-9._-]+$/.test(value))) return value;
throw new Error("Invalid managed runtime transaction path"); throw new Error("Invalid managed runtime transaction path");
} }
+1 -1
View File
@@ -319,7 +319,7 @@ function validateTransactionPath(value) {
throw managedHomeError("Invalid runtime transaction path", "RUNTIME_TRANSACTION_INVALID"); throw managedHomeError("Invalid runtime transaction path", "RUNTIME_TRANSACTION_INVALID");
} }
const normalized = value.replaceAll("\\", "/"); const normalized = value.replaceAll("\\", "/");
if (normalized === "runtime-install.json" || /^bin\/[A-Za-z0-9._-]+$/.test(normalized)) return normalized; if (normalized === "config.json" || normalized === "runtime-install.json" || /^bin\/[A-Za-z0-9._-]+$/.test(normalized)) return normalized;
throw managedHomeError(`Invalid runtime transaction path: ${value}`, "RUNTIME_TRANSACTION_INVALID"); throw managedHomeError(`Invalid runtime transaction path: ${value}`, "RUNTIME_TRANSACTION_INVALID");
} }
+83 -4
View File
@@ -123,13 +123,36 @@ export async function main(argv = process.argv.slice(2), options = {}) {
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE"); throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
} }
if (parsed.source) { if (parsed.source) {
const sourceHome = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const active = await inspectReusableRuntime(sourceHome, BOOTSTRAP_RUNTIME_VERSION).catch(() => null);
if (!browserChoice && active?.browserChoice) {
browserChoice = await resolveBrowserChoice(active.browserChoice, {
platform,
env: options.env,
homeDir: options.homeDir,
});
}
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
throw bootstrapError(
"The active browser capability does not record a reusable browser provider; choose a browser provider before changing this runtime.",
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
);
}
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
if (parsed.action === "preview") { if (parsed.action === "preview") {
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n"); io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
return 0; return 0;
} }
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED"); if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n"); io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false, browserChoice }); return await installFromSource(parsed.source, parsed, {
...options,
...io,
prepared: false,
replaceCapabilities: true,
browserChoice,
});
} }
const fetch_ = options.fetch ?? globalThis.fetch; const fetch_ = options.fetch ?? globalThis.fetch;
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
}); });
validateManifest(manifest, target); validateManifest(manifest, target);
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack")); const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null); const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
const reusable = active?.releaseMatches ? active : null;
if (!browserChoice && active?.browserChoice) {
browserChoice = await resolveBrowserChoice(active.browserChoice, {
platform,
env: options.env,
homeDir: options.homeDir,
});
}
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
throw bootstrapError(
"The active browser capability does not record a reusable browser provider; preview browser setup options before changing this runtime.",
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
);
}
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice); const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice);
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`); if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
else printComponentPlan(io.stdout, plan); else printComponentPlan(io.stdout, plan);
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
return applyBrowserProviderToComponents([...selected], browserChoice); return applyBrowserProviderToComponents([...selected], browserChoice);
} }
function mergeRetainedCapabilities(requested, reusable, browserChoice) {
const selected = new Set([
...(Array.isArray(reusable?.selectedCapabilities) ? reusable.selectedCapabilities : []),
...requested,
]);
if (browserChoice?.provider === "installed") selected.delete("browser-visible");
const pending = [...selected];
while (pending.length) {
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
if (!selected.has(dependency)) {
selected.add(dependency);
pending.push(dependency);
}
}
}
return [...selected].sort();
}
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) { function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
const components = selectedComponents(capabilities, browserChoice); const components = selectedComponents(capabilities, browserChoice);
const retained = new Set(reusable?.components ?? []); const retained = new Set(reusable?.components ?? []);
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
const stat = await fs.lstat(root); const stat = await fs.lstat(root);
if (!stat.isDirectory() || stat.isSymbolicLink()) return null; if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8")); const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) || const releaseMatches = bundle?.version === version ||
(typeof bundle?.version === "string" && bundle.version.startsWith(`${version}-caps-`));
if (bundle?.schemaVersion !== 2 || typeof bundle.version !== "string" ||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(bundle.version) || !Array.isArray(bundle.runtimeComponents) ||
!Array.isArray(bundle.files)) return null; !Array.isArray(bundle.files)) return null;
const components = [...new Set(bundle.runtimeComponents)]; const components = [...new Set(bundle.runtimeComponents)];
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null; if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
const selectedCapabilities = Array.isArray(bundle.selectedCapabilities)
? [...new Set(bundle.selectedCapabilities)]
: [];
if (selectedCapabilities.some((capability) => !CAPABILITIES.has(capability))) return null;
let browserChoice = null;
if (browserChoiceRequired(selectedCapabilities)) {
const explicit = bundle.browserChoice;
if (!explicit || !["managed", "installed"].includes(explicit.provider)) return null;
if (explicit.provider === "installed") {
if (selectedCapabilities.includes("browser-visible") ||
typeof explicit.executablePath !== "string" || !path.isAbsolute(explicit.executablePath) ||
components.includes("browser-headless") || components.includes("browser-visible")) return null;
browserChoice = { provider: "installed", executablePath: explicit.executablePath };
} else {
if (!components.includes("browser-headless") && !components.includes("browser-visible")) return null;
browserChoice = { provider: "managed", executablePath: null };
}
}
await assertNoLinks(root); await assertNoLinks(root);
const files = []; const files = [];
const seen = new Set(); const seen = new Set();
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
await sha256File(file) !== entry.sha256) return null; await sha256File(file) !== entry.sha256) return null;
files.push(relative); files.push(relative);
} }
return { root, components, files }; return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
} }
async function seedReusableRuntime(reusable, destination, claimedFiles) { async function seedReusableRuntime(reusable, destination, claimedFiles) {
@@ -496,6 +574,7 @@ async function installFromSource(source, parsed, options) {
if (parsed.home) args.push("--home", path.resolve(parsed.home)); if (parsed.home) args.push("--home", path.resolve(parsed.home));
if (options.version) args.push("--version", options.version); if (options.version) args.push("--version", options.version);
if (options.prepared) args.push("--prepared"); if (options.prepared) args.push("--prepared");
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
await run(options.nodeCommand ?? process.execPath, args); await run(options.nodeCommand ?? process.execPath, args);
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`); options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
return 0; return 0;
+67 -1
View File
@@ -2,14 +2,16 @@ import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { assertPathInside, resolveRuntimePaths } from "./paths.js"; import { assertPathInside, resolveRuntimePaths } from "./paths.js";
import { atomicWriteJson, pathExists, readJson, renameWithRetry } from "./storage.js"; import { atomicWriteFile, atomicWriteJson, pathExists, readJson, renameWithRetry } from "./storage.js";
import { import {
assertManagedHome, assertManagedHome,
ensureManagedHome, ensureManagedHome,
ensureManagedRuntimeDirectory, ensureManagedRuntimeDirectory,
recoverRuntimeTransactionUnlocked, recoverRuntimeTransactionUnlocked,
RUNTIME_TRANSACTION_FILE,
withRuntimeLifecycleLock, withRuntimeLifecycleLock,
} from "./managed-home.js"; } from "./managed-home.js";
import { configSetBrowserChoice } from "./config.js";
import { errorWithCode as upgradeError } from "./errors.js"; import { errorWithCode as upgradeError } from "./errors.js";
import { currentIsoTimestamp as isoNow } from "./time.js"; import { currentIsoTimestamp as isoNow } from "./time.js";
@@ -214,6 +216,11 @@ export async function rollbackUpgrade(home, options = {}) {
} }
await assertTreeContainsNoLinks(fallbackPath); await assertTreeContainsNoLinks(fallbackPath);
if (options.healthCheck) await options.healthCheck(fallbackPath); if (options.healthCheck) await options.healthCheck(fallbackPath);
const activation = options.prepareActivation
? await options.prepareActivation(fallbackPath)
: null;
const syncBrowserChoice = activation != null &&
Object.prototype.hasOwnProperty.call(activation, "browserChoice");
const rolledBack = { const rolledBack = {
schemaVersion: 2, schemaVersion: 2,
status: "active", status: "active",
@@ -222,11 +229,70 @@ export async function rollbackUpgrade(home, options = {}) {
rolledBackFrom: pointer.current ?? null, rolledBackFrom: pointer.current ?? null,
rolledBackAt: isoNow(options.now), rolledBackAt: isoNow(options.now),
}; };
if (!syncBrowserChoice) {
await atomicWriteJson(paths.versionPointer, rolledBack, { mode: 0o600 }); await atomicWriteJson(paths.versionPointer, rolledBack, { mode: 0o600 });
return rolledBack; return rolledBack;
}
const configSnapshot = await snapshotRollbackConfig(paths.config);
const journalPath = path.join(resolved, RUNTIME_TRANSACTION_FILE);
await atomicWriteJson(journalPath, {
schemaVersion: 1,
kind: "gstack-runtime-install-transaction",
status: "prepared",
home: resolved,
version: fallbackVersion,
previousPointerExists: true,
previousPointer: pointer,
files: [configSnapshot == null
? { path: "config.json", existed: false }
: {
path: "config.json",
existed: true,
mode: configSnapshot.mode,
dataBase64: configSnapshot.data.toString("base64"),
}],
preparedAt: isoNow(options.now),
}, { mode: 0o600 });
try {
await configSetBrowserChoice(resolved, activation.browserChoice);
await atomicWriteJson(paths.versionPointer, rolledBack, { mode: 0o600 });
await fs.rm(journalPath, { force: true });
} catch (cause) {
const rollbackErrors = [];
try {
if (configSnapshot == null) await fs.rm(paths.config, { force: true });
else await atomicWriteFile(paths.config, configSnapshot.data, { mode: configSnapshot.mode });
} catch (error) {
rollbackErrors.push(error);
}
try {
await atomicWriteJson(paths.versionPointer, pointer, { mode: 0o600 });
} catch (error) {
rollbackErrors.push(error);
}
if (rollbackErrors.length === 0) await fs.rm(journalPath, { force: true });
const error = upgradeError("Rollback activation failed and the previous runtime was restored", "ROLLBACK_ACTIVATION_FAILED", cause);
if (rollbackErrors.length === 1) error.rollbackError = rollbackErrors[0];
else if (rollbackErrors.length > 1) error.rollbackError = new AggregateError(rollbackErrors, "Runtime rollback restoration was incomplete");
throw error;
}
return rolledBack;
}, options); }, options);
} }
async function snapshotRollbackConfig(configPath) {
const stat = await fs.lstat(configPath).catch((error) => {
if (error?.code === "ENOENT") return null;
throw error;
});
if (!stat) return null;
if (!stat.isFile() || stat.isSymbolicLink()) {
throw upgradeError("Refusing unsafe browser configuration during rollback", "RUNTIME_TRANSACTION_INVALID");
}
return { data: await fs.readFile(configPath), mode: stat.mode & 0o777 };
}
export async function activeVersion(home, options = {}) { export async function activeVersion(home, options = {}) {
const recovered = await recoverPendingUpgrade(home, options); const recovered = await recoverPendingUpgrade(home, options);
return recovered.pointer; return recovered.pointer;
+1 -1
View File
@@ -38,7 +38,7 @@ CHROMIUM_DIR=$(ls -d "$PW_CACHE"/chromium-*/chrome-mac-arm64 2>/dev/null | sort
if [ -z "$CHROMIUM_DIR" ]; then if [ -z "$CHROMIUM_DIR" ]; then
echo "ERROR: Playwright Chromium not found in $PW_CACHE" echo "ERROR: Playwright Chromium not found in $PW_CACHE"
echo "Run: bunx playwright install chromium" echo "Run: bunx playwright-core install chromium"
exit 1 exit 1
fi fi
+2 -1
View File
@@ -311,7 +311,8 @@ function portLegacyText(value: string, source: string): string {
.replaceAll('bun run $GSTACK_BIN/gstack-next-version', '$GSTACK_BIN/gstack-next-version') .replaceAll('bun run $GSTACK_BIN/gstack-next-version', '$GSTACK_BIN/gstack-next-version')
.replaceAll('bun run $GSTACK_BIN/gstack-version-bump', '$GSTACK_BIN/gstack-version-bump') .replaceAll('bun run $GSTACK_BIN/gstack-version-bump', '$GSTACK_BIN/gstack-version-bump')
.replaceAll('DISCOVER_BIN="bun run $GSTACK_BIN/gstack-global-discover"', 'DISCOVER_BIN="$GSTACK_BIN/gstack-global-discover"') .replaceAll('DISCOVER_BIN="bun run $GSTACK_BIN/gstack-global-discover"', 'DISCOVER_BIN="$GSTACK_BIN/gstack-global-discover"')
.replaceAll('Tell the user: "gstack browse needs a one-time build (~10 seconds). OK to proceed?" Then STOP and wait.', 'Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.') .replaceAll('Tell the user: "gstack browse needs a one-time build (~10 seconds). OK to proceed?" Then STOP and wait.', 'Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.')
.replaceAll('Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.', 'Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.')
.replace(/^command -v bun >\/dev\/null 2>&1 \|\| echo "redaction scan skipped — bun not on PATH"\n/gm, ''); .replace(/^command -v bun >\/dev\/null 2>&1 \|\| echo "redaction scan skipped — bun not on PATH"\n/gm, '');
body = body body = body
@@ -123,13 +123,36 @@ export async function main(argv = process.argv.slice(2), options = {}) {
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE"); throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
} }
if (parsed.source) { if (parsed.source) {
const sourceHome = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const active = await inspectReusableRuntime(sourceHome, BOOTSTRAP_RUNTIME_VERSION).catch(() => null);
if (!browserChoice && active?.browserChoice) {
browserChoice = await resolveBrowserChoice(active.browserChoice, {
platform,
env: options.env,
homeDir: options.homeDir,
});
}
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
throw bootstrapError(
"The active browser capability does not record a reusable browser provider; choose a browser provider before changing this runtime.",
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
);
}
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
if (parsed.action === "preview") { if (parsed.action === "preview") {
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n"); io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
return 0; return 0;
} }
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED"); if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n"); io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false, browserChoice }); return await installFromSource(parsed.source, parsed, {
...options,
...io,
prepared: false,
replaceCapabilities: true,
browserChoice,
});
} }
const fetch_ = options.fetch ?? globalThis.fetch; const fetch_ = options.fetch ?? globalThis.fetch;
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
}); });
validateManifest(manifest, target); validateManifest(manifest, target);
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack")); const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null); const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
const reusable = active?.releaseMatches ? active : null;
if (!browserChoice && active?.browserChoice) {
browserChoice = await resolveBrowserChoice(active.browserChoice, {
platform,
env: options.env,
homeDir: options.homeDir,
});
}
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
throw bootstrapError(
"The active browser capability does not record a reusable browser provider; preview browser setup options before changing this runtime.",
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
);
}
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice); const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice);
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`); if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
else printComponentPlan(io.stdout, plan); else printComponentPlan(io.stdout, plan);
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
return applyBrowserProviderToComponents([...selected], browserChoice); return applyBrowserProviderToComponents([...selected], browserChoice);
} }
function mergeRetainedCapabilities(requested, reusable, browserChoice) {
const selected = new Set([
...(Array.isArray(reusable?.selectedCapabilities) ? reusable.selectedCapabilities : []),
...requested,
]);
if (browserChoice?.provider === "installed") selected.delete("browser-visible");
const pending = [...selected];
while (pending.length) {
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
if (!selected.has(dependency)) {
selected.add(dependency);
pending.push(dependency);
}
}
}
return [...selected].sort();
}
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) { function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
const components = selectedComponents(capabilities, browserChoice); const components = selectedComponents(capabilities, browserChoice);
const retained = new Set(reusable?.components ?? []); const retained = new Set(reusable?.components ?? []);
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
const stat = await fs.lstat(root); const stat = await fs.lstat(root);
if (!stat.isDirectory() || stat.isSymbolicLink()) return null; if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8")); const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) || const releaseMatches = bundle?.version === version ||
(typeof bundle?.version === "string" && bundle.version.startsWith(`${version}-caps-`));
if (bundle?.schemaVersion !== 2 || typeof bundle.version !== "string" ||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(bundle.version) || !Array.isArray(bundle.runtimeComponents) ||
!Array.isArray(bundle.files)) return null; !Array.isArray(bundle.files)) return null;
const components = [...new Set(bundle.runtimeComponents)]; const components = [...new Set(bundle.runtimeComponents)];
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null; if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
const selectedCapabilities = Array.isArray(bundle.selectedCapabilities)
? [...new Set(bundle.selectedCapabilities)]
: [];
if (selectedCapabilities.some((capability) => !CAPABILITIES.has(capability))) return null;
let browserChoice = null;
if (browserChoiceRequired(selectedCapabilities)) {
const explicit = bundle.browserChoice;
if (!explicit || !["managed", "installed"].includes(explicit.provider)) return null;
if (explicit.provider === "installed") {
if (selectedCapabilities.includes("browser-visible") ||
typeof explicit.executablePath !== "string" || !path.isAbsolute(explicit.executablePath) ||
components.includes("browser-headless") || components.includes("browser-visible")) return null;
browserChoice = { provider: "installed", executablePath: explicit.executablePath };
} else {
if (!components.includes("browser-headless") && !components.includes("browser-visible")) return null;
browserChoice = { provider: "managed", executablePath: null };
}
}
await assertNoLinks(root); await assertNoLinks(root);
const files = []; const files = [];
const seen = new Set(); const seen = new Set();
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
await sha256File(file) !== entry.sha256) return null; await sha256File(file) !== entry.sha256) return null;
files.push(relative); files.push(relative);
} }
return { root, components, files }; return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
} }
async function seedReusableRuntime(reusable, destination, claimedFiles) { async function seedReusableRuntime(reusable, destination, claimedFiles) {
@@ -496,6 +574,7 @@ async function installFromSource(source, parsed, options) {
if (parsed.home) args.push("--home", path.resolve(parsed.home)); if (parsed.home) args.push("--home", path.resolve(parsed.home));
if (options.version) args.push("--version", options.version); if (options.version) args.push("--version", options.version);
if (options.prepared) args.push("--prepared"); if (options.prepared) args.push("--prepared");
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
await run(options.nodeCommand ?? process.execPath, args); await run(options.nodeCommand ?? process.execPath, args);
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`); options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
return 0; return 0;
@@ -1,5 +1,5 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. --> <!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
<!-- GSTACK2_PROVENANCE source=design-consultation/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=64af56ecdbd132cb7c28344e8e4ecb2e5dacf811 baseline_render_sha256=62b8141e0b3edb26dcfd175c25c7021d4713b64add121137ace0a123e6e6ea8a ported_render_sha256=d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b disposition=BUG_FIX --> <!-- GSTACK2_PROVENANCE source=design-consultation/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=64af56ecdbd132cb7c28344e8e4ecb2e5dacf811 baseline_render_sha256=62b8141e0b3edb26dcfd175c25c7021d4713b64add121137ace0a123e6e6ea8a ported_render_sha256=13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b disposition=BUG_FIX -->
<!-- GSTACK2_ROUTING replacement=$design --mode Generate --module design-consultation visibility=primary depth=deep mutation=design-artifacts web=optional --> <!-- GSTACK2_ROUTING replacement=$design --mode Generate --module design-consultation visibility=primary depth=deep mutation=design-artifacts web=optional -->
<!-- GSTACK2_LEGACY_BODY_START source=design-consultation --> <!-- GSTACK2_LEGACY_BODY_START source=design-consultation -->
@@ -74,7 +74,7 @@ fi
``` ```
If `NEEDS_SETUP`: If `NEEDS_SETUP`:
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait. 1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`. 2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow. 3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
@@ -1,5 +1,5 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. --> <!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
<!-- GSTACK2_PROVENANCE source=design-html/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=3cdec9a14d62d2e046ed924c972efc30a7d43aca baseline_render_sha256=d16ec32f4c07da49d32efc309e621b514854ce355db8347647b9f9fc215ff66d ported_render_sha256=40682d97ac83aa9178487348d5abf176334fd439e2d12f8e5cda1f8b20cd2c30 disposition=BUG_FIX --> <!-- GSTACK2_PROVENANCE source=design-html/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=3cdec9a14d62d2e046ed924c972efc30a7d43aca baseline_render_sha256=d16ec32f4c07da49d32efc309e621b514854ce355db8347647b9f9fc215ff66d ported_render_sha256=775dfc9fdcc6b96d6e267f2b8c5e7eedcf8a1d98b764c134d7111a39f3f07301 disposition=BUG_FIX -->
<!-- GSTACK2_ROUTING replacement=$design --mode Implement --module design-html visibility=primary depth=standard mutation=design-artifacts web=local-browser --> <!-- GSTACK2_ROUTING replacement=$design --mode Implement --module design-html visibility=primary depth=standard mutation=design-artifacts web=local-browser -->
<!-- GSTACK2_LEGACY_BODY_START source=design-html --> <!-- GSTACK2_LEGACY_BODY_START source=design-html -->
@@ -167,7 +167,7 @@ fi
``` ```
If `NEEDS_SETUP`: If `NEEDS_SETUP`:
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait. 1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`. 2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow. 3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
@@ -1,5 +1,5 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. --> <!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
<!-- GSTACK2_PROVENANCE source=design-review/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=bdcda48e29b489a1cc49faa333922412251d4b41 baseline_render_sha256=ff6d5d4858ed45db1e9581080739c0b4c5029bca44ecabe0c385637ece68e0cb ported_render_sha256=fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b disposition=BUG_FIX --> <!-- GSTACK2_PROVENANCE source=design-review/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=bdcda48e29b489a1cc49faa333922412251d4b41 baseline_render_sha256=ff6d5d4858ed45db1e9581080739c0b4c5029bca44ecabe0c385637ece68e0cb ported_render_sha256=9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900 disposition=BUG_FIX -->
<!-- GSTACK2_ROUTING replacement=$design --mode Implement --module design-review visibility=primary depth=deep mutation=fix-safe web=local-browser --> <!-- GSTACK2_ROUTING replacement=$design --mode Implement --module design-review visibility=primary depth=deep mutation=fix-safe web=local-browser -->
<!-- GSTACK2_LEGACY_BODY_START source=design-review --> <!-- GSTACK2_LEGACY_BODY_START source=design-review -->
@@ -81,7 +81,7 @@ fi
``` ```
If `NEEDS_SETUP`: If `NEEDS_SETUP`:
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait. 1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`. 2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow. 3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
@@ -123,13 +123,36 @@ export async function main(argv = process.argv.slice(2), options = {}) {
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE"); throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
} }
if (parsed.source) { if (parsed.source) {
const sourceHome = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const active = await inspectReusableRuntime(sourceHome, BOOTSTRAP_RUNTIME_VERSION).catch(() => null);
if (!browserChoice && active?.browserChoice) {
browserChoice = await resolveBrowserChoice(active.browserChoice, {
platform,
env: options.env,
homeDir: options.homeDir,
});
}
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
throw bootstrapError(
"The active browser capability does not record a reusable browser provider; choose a browser provider before changing this runtime.",
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
);
}
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
if (parsed.action === "preview") { if (parsed.action === "preview") {
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n"); io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
return 0; return 0;
} }
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED"); if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n"); io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false, browserChoice }); return await installFromSource(parsed.source, parsed, {
...options,
...io,
prepared: false,
replaceCapabilities: true,
browserChoice,
});
} }
const fetch_ = options.fetch ?? globalThis.fetch; const fetch_ = options.fetch ?? globalThis.fetch;
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
}); });
validateManifest(manifest, target); validateManifest(manifest, target);
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack")); const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null); const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
const reusable = active?.releaseMatches ? active : null;
if (!browserChoice && active?.browserChoice) {
browserChoice = await resolveBrowserChoice(active.browserChoice, {
platform,
env: options.env,
homeDir: options.homeDir,
});
}
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
throw bootstrapError(
"The active browser capability does not record a reusable browser provider; preview browser setup options before changing this runtime.",
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
);
}
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice); const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice);
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`); if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
else printComponentPlan(io.stdout, plan); else printComponentPlan(io.stdout, plan);
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
return applyBrowserProviderToComponents([...selected], browserChoice); return applyBrowserProviderToComponents([...selected], browserChoice);
} }
function mergeRetainedCapabilities(requested, reusable, browserChoice) {
const selected = new Set([
...(Array.isArray(reusable?.selectedCapabilities) ? reusable.selectedCapabilities : []),
...requested,
]);
if (browserChoice?.provider === "installed") selected.delete("browser-visible");
const pending = [...selected];
while (pending.length) {
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
if (!selected.has(dependency)) {
selected.add(dependency);
pending.push(dependency);
}
}
}
return [...selected].sort();
}
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) { function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
const components = selectedComponents(capabilities, browserChoice); const components = selectedComponents(capabilities, browserChoice);
const retained = new Set(reusable?.components ?? []); const retained = new Set(reusable?.components ?? []);
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
const stat = await fs.lstat(root); const stat = await fs.lstat(root);
if (!stat.isDirectory() || stat.isSymbolicLink()) return null; if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8")); const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) || const releaseMatches = bundle?.version === version ||
(typeof bundle?.version === "string" && bundle.version.startsWith(`${version}-caps-`));
if (bundle?.schemaVersion !== 2 || typeof bundle.version !== "string" ||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(bundle.version) || !Array.isArray(bundle.runtimeComponents) ||
!Array.isArray(bundle.files)) return null; !Array.isArray(bundle.files)) return null;
const components = [...new Set(bundle.runtimeComponents)]; const components = [...new Set(bundle.runtimeComponents)];
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null; if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
const selectedCapabilities = Array.isArray(bundle.selectedCapabilities)
? [...new Set(bundle.selectedCapabilities)]
: [];
if (selectedCapabilities.some((capability) => !CAPABILITIES.has(capability))) return null;
let browserChoice = null;
if (browserChoiceRequired(selectedCapabilities)) {
const explicit = bundle.browserChoice;
if (!explicit || !["managed", "installed"].includes(explicit.provider)) return null;
if (explicit.provider === "installed") {
if (selectedCapabilities.includes("browser-visible") ||
typeof explicit.executablePath !== "string" || !path.isAbsolute(explicit.executablePath) ||
components.includes("browser-headless") || components.includes("browser-visible")) return null;
browserChoice = { provider: "installed", executablePath: explicit.executablePath };
} else {
if (!components.includes("browser-headless") && !components.includes("browser-visible")) return null;
browserChoice = { provider: "managed", executablePath: null };
}
}
await assertNoLinks(root); await assertNoLinks(root);
const files = []; const files = [];
const seen = new Set(); const seen = new Set();
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
await sha256File(file) !== entry.sha256) return null; await sha256File(file) !== entry.sha256) return null;
files.push(relative); files.push(relative);
} }
return { root, components, files }; return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
} }
async function seedReusableRuntime(reusable, destination, claimedFiles) { async function seedReusableRuntime(reusable, destination, claimedFiles) {
@@ -496,6 +574,7 @@ async function installFromSource(source, parsed, options) {
if (parsed.home) args.push("--home", path.resolve(parsed.home)); if (parsed.home) args.push("--home", path.resolve(parsed.home));
if (options.version) args.push("--version", options.version); if (options.version) args.push("--version", options.version);
if (options.prepared) args.push("--prepared"); if (options.prepared) args.push("--prepared");
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
await run(options.nodeCommand ?? process.execPath, args); await run(options.nodeCommand ?? process.execPath, args);
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`); options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
return 0; return 0;
@@ -1,5 +1,5 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. --> <!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
<!-- GSTACK2_PROVENANCE source=office-hours/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=8568fe73cca76a80805fab3092cacd10db7e1d7f baseline_render_sha256=5af4dc503ee149ac5052617ec4d5ad1947c9fbf28c663f40457b0eb07f5fcea3 ported_render_sha256=1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052 disposition=BUG_FIX --> <!-- GSTACK2_PROVENANCE source=office-hours/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=8568fe73cca76a80805fab3092cacd10db7e1d7f baseline_render_sha256=5af4dc503ee149ac5052617ec4d5ad1947c9fbf28c663f40457b0eb07f5fcea3 ported_render_sha256=ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba disposition=BUG_FIX -->
<!-- GSTACK2_ROUTING replacement=$plan --mode Discovery --module office-hours visibility=primary depth=deep mutation=design-doc-only web=optional --> <!-- GSTACK2_ROUTING replacement=$plan --mode Discovery --module office-hours visibility=primary depth=deep mutation=design-doc-only web=optional -->
<!-- GSTACK2_LEGACY_BODY_START source=office-hours --> <!-- GSTACK2_LEGACY_BODY_START source=office-hours -->
@@ -32,7 +32,7 @@ fi
``` ```
If `NEEDS_SETUP`: If `NEEDS_SETUP`:
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait. 1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`. 2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow. 3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
@@ -123,13 +123,36 @@ export async function main(argv = process.argv.slice(2), options = {}) {
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE"); throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
} }
if (parsed.source) { if (parsed.source) {
const sourceHome = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const active = await inspectReusableRuntime(sourceHome, BOOTSTRAP_RUNTIME_VERSION).catch(() => null);
if (!browserChoice && active?.browserChoice) {
browserChoice = await resolveBrowserChoice(active.browserChoice, {
platform,
env: options.env,
homeDir: options.homeDir,
});
}
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
throw bootstrapError(
"The active browser capability does not record a reusable browser provider; choose a browser provider before changing this runtime.",
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
);
}
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
if (parsed.action === "preview") { if (parsed.action === "preview") {
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n"); io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
return 0; return 0;
} }
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED"); if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n"); io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false, browserChoice }); return await installFromSource(parsed.source, parsed, {
...options,
...io,
prepared: false,
replaceCapabilities: true,
browserChoice,
});
} }
const fetch_ = options.fetch ?? globalThis.fetch; const fetch_ = options.fetch ?? globalThis.fetch;
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
}); });
validateManifest(manifest, target); validateManifest(manifest, target);
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack")); const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null); const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
const reusable = active?.releaseMatches ? active : null;
if (!browserChoice && active?.browserChoice) {
browserChoice = await resolveBrowserChoice(active.browserChoice, {
platform,
env: options.env,
homeDir: options.homeDir,
});
}
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
throw bootstrapError(
"The active browser capability does not record a reusable browser provider; preview browser setup options before changing this runtime.",
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
);
}
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice); const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice);
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`); if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
else printComponentPlan(io.stdout, plan); else printComponentPlan(io.stdout, plan);
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
return applyBrowserProviderToComponents([...selected], browserChoice); return applyBrowserProviderToComponents([...selected], browserChoice);
} }
function mergeRetainedCapabilities(requested, reusable, browserChoice) {
const selected = new Set([
...(Array.isArray(reusable?.selectedCapabilities) ? reusable.selectedCapabilities : []),
...requested,
]);
if (browserChoice?.provider === "installed") selected.delete("browser-visible");
const pending = [...selected];
while (pending.length) {
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
if (!selected.has(dependency)) {
selected.add(dependency);
pending.push(dependency);
}
}
}
return [...selected].sort();
}
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) { function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
const components = selectedComponents(capabilities, browserChoice); const components = selectedComponents(capabilities, browserChoice);
const retained = new Set(reusable?.components ?? []); const retained = new Set(reusable?.components ?? []);
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
const stat = await fs.lstat(root); const stat = await fs.lstat(root);
if (!stat.isDirectory() || stat.isSymbolicLink()) return null; if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8")); const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) || const releaseMatches = bundle?.version === version ||
(typeof bundle?.version === "string" && bundle.version.startsWith(`${version}-caps-`));
if (bundle?.schemaVersion !== 2 || typeof bundle.version !== "string" ||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(bundle.version) || !Array.isArray(bundle.runtimeComponents) ||
!Array.isArray(bundle.files)) return null; !Array.isArray(bundle.files)) return null;
const components = [...new Set(bundle.runtimeComponents)]; const components = [...new Set(bundle.runtimeComponents)];
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null; if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
const selectedCapabilities = Array.isArray(bundle.selectedCapabilities)
? [...new Set(bundle.selectedCapabilities)]
: [];
if (selectedCapabilities.some((capability) => !CAPABILITIES.has(capability))) return null;
let browserChoice = null;
if (browserChoiceRequired(selectedCapabilities)) {
const explicit = bundle.browserChoice;
if (!explicit || !["managed", "installed"].includes(explicit.provider)) return null;
if (explicit.provider === "installed") {
if (selectedCapabilities.includes("browser-visible") ||
typeof explicit.executablePath !== "string" || !path.isAbsolute(explicit.executablePath) ||
components.includes("browser-headless") || components.includes("browser-visible")) return null;
browserChoice = { provider: "installed", executablePath: explicit.executablePath };
} else {
if (!components.includes("browser-headless") && !components.includes("browser-visible")) return null;
browserChoice = { provider: "managed", executablePath: null };
}
}
await assertNoLinks(root); await assertNoLinks(root);
const files = []; const files = [];
const seen = new Set(); const seen = new Set();
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
await sha256File(file) !== entry.sha256) return null; await sha256File(file) !== entry.sha256) return null;
files.push(relative); files.push(relative);
} }
return { root, components, files }; return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
} }
async function seedReusableRuntime(reusable, destination, claimedFiles) { async function seedReusableRuntime(reusable, destination, claimedFiles) {
@@ -496,6 +574,7 @@ async function installFromSource(source, parsed, options) {
if (parsed.home) args.push("--home", path.resolve(parsed.home)); if (parsed.home) args.push("--home", path.resolve(parsed.home));
if (options.version) args.push("--version", options.version); if (options.version) args.push("--version", options.version);
if (options.prepared) args.push("--prepared"); if (options.prepared) args.push("--prepared");
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
await run(options.nodeCommand ?? process.execPath, args); await run(options.nodeCommand ?? process.execPath, args);
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`); options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
return 0; return 0;
+2 -2
View File
@@ -1,5 +1,5 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. --> <!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
<!-- GSTACK2_PROVENANCE source=benchmark/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=038f16f5fba4ae4e9eae922e3276bba8ef88149e baseline_render_sha256=c1a8019b9b430790f0917df8d58e7a645f01f2784398343d64e5505b535c1ea7 ported_render_sha256=05ac1b123a605201546a7e95899a5b55708ca7fbb7569c58b4d755c52b45a92d disposition=BUG_FIX --> <!-- GSTACK2_PROVENANCE source=benchmark/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=038f16f5fba4ae4e9eae922e3276bba8ef88149e baseline_render_sha256=c1a8019b9b430790f0917df8d58e7a645f01f2784398343d64e5505b535c1ea7 ported_render_sha256=5fd14a7da7e31c24451c26d9123fcbcc376b7875c1a723bf7b69a1474e4f1c6d disposition=BUG_FIX -->
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module benchmark visibility=primary depth=standard mutation=report-only web=local-browser --> <!-- GSTACK2_ROUTING replacement=$qa --mode Report --module benchmark visibility=primary depth=standard mutation=report-only web=local-browser -->
<!-- GSTACK2_LEGACY_BODY_START source=benchmark --> <!-- GSTACK2_LEGACY_BODY_START source=benchmark -->
@@ -32,7 +32,7 @@ fi
``` ```
If `NEEDS_SETUP`: If `NEEDS_SETUP`:
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait. 1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`. 2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow. 3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
+2 -2
View File
@@ -1,5 +1,5 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. --> <!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
<!-- GSTACK2_PROVENANCE source=browse/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=9a159e4c9820172c229e2174d4a62a8f9668ab93 baseline_render_sha256=26c248b90f91a99d1e31e51afaec46385941ab2071bfab7bfdf6d044151ab3ac ported_render_sha256=1b532bd904b1fa1686113e8c96b70015ea6b2e6df7319a72c299de901fe5e81b disposition=BUG_FIX --> <!-- GSTACK2_PROVENANCE source=browse/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=9a159e4c9820172c229e2174d4a62a8f9668ab93 baseline_render_sha256=26c248b90f91a99d1e31e51afaec46385941ab2071bfab7bfdf6d044151ab3ac ported_render_sha256=fee4ae0bd69412a6c3b1fd6737064301240b39731695b53fa10096ba48495019 disposition=BUG_FIX -->
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module browse visibility=internal depth=standard mutation=source-defined web=local-browser --> <!-- GSTACK2_ROUTING replacement=$qa --mode Report --module browse visibility=internal depth=standard mutation=source-defined web=local-browser -->
<!-- GSTACK2_LEGACY_BODY_START source=browse --> <!-- GSTACK2_LEGACY_BODY_START source=browse -->
@@ -37,7 +37,7 @@ fi
``` ```
If `NEEDS_SETUP`: If `NEEDS_SETUP`:
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait. 1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`. 2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow. 3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
+2 -2
View File
@@ -1,5 +1,5 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. --> <!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
<!-- GSTACK2_PROVENANCE source=canary/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=d1eb2950aba2fa2b09d90f13143492c60d46793c baseline_render_sha256=8dd0ff918566e1c5536f1bfcc546eebbdbee5e43d0a05b14a8efb898d3574dbe ported_render_sha256=89be5f218da2bd812303c87b8c177081727727e5e0d2dc74eb7a73299794d5ef disposition=BUG_FIX --> <!-- GSTACK2_PROVENANCE source=canary/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=d1eb2950aba2fa2b09d90f13143492c60d46793c baseline_render_sha256=8dd0ff918566e1c5536f1bfcc546eebbdbee5e43d0a05b14a8efb898d3574dbe ported_render_sha256=b7f753ba0b98d8c7378dc797dca5950b14ebe26565bac25b5bbaa56b8ea8e13b disposition=BUG_FIX -->
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module canary visibility=primary depth=deep mutation=report-only web=production --> <!-- GSTACK2_ROUTING replacement=$qa --mode Report --module canary visibility=primary depth=deep mutation=report-only web=production -->
<!-- GSTACK2_LEGACY_BODY_START source=canary --> <!-- GSTACK2_LEGACY_BODY_START source=canary -->
@@ -32,7 +32,7 @@ fi
``` ```
If `NEEDS_SETUP`: If `NEEDS_SETUP`:
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait. 1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`. 2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow. 3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
+2 -2
View File
@@ -1,5 +1,5 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. --> <!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
<!-- GSTACK2_PROVENANCE source=devex-review/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=081d4f35bbdec0c6b3da8ae71615ec4d41a84551 baseline_render_sha256=d070b5d50c0b8a59efc7be04881734419f815f01b065ee9d15cf151dba9afb18 ported_render_sha256=4a907c759b6cf4202fbacaea504b1eb601a53dd35b206109d6c5105168ade7e1 disposition=BUG_FIX --> <!-- GSTACK2_PROVENANCE source=devex-review/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=081d4f35bbdec0c6b3da8ae71615ec4d41a84551 baseline_render_sha256=d070b5d50c0b8a59efc7be04881734419f815f01b065ee9d15cf151dba9afb18 ported_render_sha256=6b26b22ae5cbe9483a10ad084cd6b1e8ea32d2c01e482f75b9f8b32944287d0e disposition=BUG_FIX -->
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module devex-review visibility=primary depth=deep mutation=report-only web=optional --> <!-- GSTACK2_ROUTING replacement=$qa --mode Report --module devex-review visibility=primary depth=deep mutation=report-only web=optional -->
<!-- GSTACK2_LEGACY_BODY_START source=devex-review --> <!-- GSTACK2_LEGACY_BODY_START source=devex-review -->
@@ -71,7 +71,7 @@ fi
``` ```
If `NEEDS_SETUP`: If `NEEDS_SETUP`:
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait. 1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`. 2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow. 3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
@@ -1,5 +1,5 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. --> <!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
<!-- GSTACK2_PROVENANCE source=open-gstack-browser/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=ef91a527890a3ac3622cc7dc84bad1ff7b64443b baseline_render_sha256=f68b483619f37175687c64510c4de5c718ad3aa2d644134f6539d28df3a9ad7c ported_render_sha256=e6e8271ecd89761627e6e67745750b22e64596d0e51e4a2350dccd8e2ce8ebd6 disposition=BUG_FIX --> <!-- GSTACK2_PROVENANCE source=open-gstack-browser/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=ef91a527890a3ac3622cc7dc84bad1ff7b64443b baseline_render_sha256=f68b483619f37175687c64510c4de5c718ad3aa2d644134f6539d28df3a9ad7c ported_render_sha256=f32ab85292ae920d811f4014480c48941d9b34d3ad8141840b4fd33bfdccc7dd disposition=BUG_FIX -->
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module open-gstack-browser visibility=internal depth=standard mutation=configuration web=local-browser --> <!-- GSTACK2_ROUTING replacement=$qa --mode Report --module open-gstack-browser visibility=internal depth=standard mutation=configuration web=local-browser -->
<!-- GSTACK2_LEGACY_BODY_START source=open-gstack-browser --> <!-- GSTACK2_LEGACY_BODY_START source=open-gstack-browser -->
@@ -43,7 +43,7 @@ fi
``` ```
If `NEEDS_SETUP`: If `NEEDS_SETUP`:
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait. 1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`. 2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow. 3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
+2 -2
View File
@@ -1,5 +1,5 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. --> <!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
<!-- GSTACK2_PROVENANCE source=pair-agent/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=75ed42d590f99c46cd0883c37bb1f2f9f499211c baseline_render_sha256=6bb659c03b5df7c36f446fad30aaec4ab6d5e0d25fb8392702573e66923b02fb ported_render_sha256=256fd576911cc286ddd2510daec8f4c68501cc5534f46edc044c1908574ac64a disposition=BUG_FIX --> <!-- GSTACK2_PROVENANCE source=pair-agent/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=75ed42d590f99c46cd0883c37bb1f2f9f499211c baseline_render_sha256=6bb659c03b5df7c36f446fad30aaec4ab6d5e0d25fb8392702573e66923b02fb ported_render_sha256=e75661246495412102632a49d626bc313875ef479d2c570002ec66a2ccd2757a disposition=BUG_FIX -->
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module pair-agent visibility=internal depth=standard mutation=configuration web=local-browser --> <!-- GSTACK2_ROUTING replacement=$qa --mode Report --module pair-agent visibility=internal depth=standard mutation=configuration web=local-browser -->
<!-- GSTACK2_LEGACY_BODY_START source=pair-agent --> <!-- GSTACK2_LEGACY_BODY_START source=pair-agent -->
@@ -61,7 +61,7 @@ fi
``` ```
If `NEEDS_SETUP`: If `NEEDS_SETUP`:
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait. 1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`. 2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow. 3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
+2 -2
View File
@@ -1,5 +1,5 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. --> <!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
<!-- GSTACK2_PROVENANCE source=qa-only/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=75c4123cc5c406ffdd36c71a094335c137135b1e baseline_render_sha256=7f8c42379e748156bf131a5bae121ab9a3f307e57619e261f5db7865eead3029 ported_render_sha256=376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3 disposition=BUG_FIX --> <!-- GSTACK2_PROVENANCE source=qa-only/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=75c4123cc5c406ffdd36c71a094335c137135b1e baseline_render_sha256=7f8c42379e748156bf131a5bae121ab9a3f307e57619e261f5db7865eead3029 ported_render_sha256=601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a disposition=BUG_FIX -->
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module qa-only visibility=primary depth=deep mutation=report-only web=local-browser --> <!-- GSTACK2_ROUTING replacement=$qa --mode Report --module qa-only visibility=primary depth=deep mutation=report-only web=local-browser -->
<!-- GSTACK2_LEGACY_BODY_START source=qa-only --> <!-- GSTACK2_LEGACY_BODY_START source=qa-only -->
@@ -52,7 +52,7 @@ fi
``` ```
If `NEEDS_SETUP`: If `NEEDS_SETUP`:
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait. 1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`. 2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow. 3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
+2 -2
View File
@@ -1,5 +1,5 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. --> <!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
<!-- GSTACK2_PROVENANCE source=qa/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=11997f7b878282c34b6bfd3d4b7a8131f9ad4da8 baseline_render_sha256=07e2c6a841c6701d186b3b6536cfdb87af49566029971502065f636576ba071c ported_render_sha256=e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55 disposition=BUG_FIX --> <!-- GSTACK2_PROVENANCE source=qa/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=11997f7b878282c34b6bfd3d4b7a8131f9ad4da8 baseline_render_sha256=07e2c6a841c6701d186b3b6536cfdb87af49566029971502065f636576ba071c ported_render_sha256=63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848 disposition=BUG_FIX -->
<!-- GSTACK2_ROUTING replacement=$qa --mode Fix --module qa visibility=primary depth=deep mutation=fix-safe web=local-browser --> <!-- GSTACK2_ROUTING replacement=$qa --mode Fix --module qa visibility=primary depth=deep mutation=fix-safe web=local-browser -->
<!-- GSTACK2_LEGACY_BODY_START source=qa --> <!-- GSTACK2_LEGACY_BODY_START source=qa -->
@@ -123,7 +123,7 @@ fi
``` ```
If `NEEDS_SETUP`: If `NEEDS_SETUP`:
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait. 1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`. 2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow. 3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
@@ -1,5 +1,5 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. --> <!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
<!-- GSTACK2_PROVENANCE source=setup-browser-cookies/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=f812d9f56f27c32fb5f102083bbe418344c1a652 baseline_render_sha256=22b03503fa8ba63de98866d64ab0563291f4b41e1577c8022d09add3e0bdb59c ported_render_sha256=7d539b2113f8cc9bf0b8b2f6e1da3dde7028176a6f71de8f47de0a98c45663e8 disposition=BUG_FIX --> <!-- GSTACK2_PROVENANCE source=setup-browser-cookies/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=f812d9f56f27c32fb5f102083bbe418344c1a652 baseline_render_sha256=22b03503fa8ba63de98866d64ab0563291f4b41e1577c8022d09add3e0bdb59c ported_render_sha256=9e8ee39b557d1fbd032a94f5fbe16b675bdd89fe64b1c85a9af6a2ebe54aa976 disposition=BUG_FIX -->
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module setup-browser-cookies visibility=internal depth=standard mutation=configuration web=local-browser --> <!-- GSTACK2_ROUTING replacement=$qa --mode Report --module setup-browser-cookies visibility=internal depth=standard mutation=configuration web=local-browser -->
<!-- GSTACK2_LEGACY_BODY_START source=setup-browser-cookies --> <!-- GSTACK2_LEGACY_BODY_START source=setup-browser-cookies -->
@@ -61,7 +61,7 @@ fi
``` ```
If `NEEDS_SETUP`: If `NEEDS_SETUP`:
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait. 1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`. 2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow. 3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
@@ -123,13 +123,36 @@ export async function main(argv = process.argv.slice(2), options = {}) {
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE"); throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
} }
if (parsed.source) { if (parsed.source) {
const sourceHome = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const active = await inspectReusableRuntime(sourceHome, BOOTSTRAP_RUNTIME_VERSION).catch(() => null);
if (!browserChoice && active?.browserChoice) {
browserChoice = await resolveBrowserChoice(active.browserChoice, {
platform,
env: options.env,
homeDir: options.homeDir,
});
}
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
throw bootstrapError(
"The active browser capability does not record a reusable browser provider; choose a browser provider before changing this runtime.",
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
);
}
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
if (parsed.action === "preview") { if (parsed.action === "preview") {
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n"); io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
return 0; return 0;
} }
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED"); if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n"); io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false, browserChoice }); return await installFromSource(parsed.source, parsed, {
...options,
...io,
prepared: false,
replaceCapabilities: true,
browserChoice,
});
} }
const fetch_ = options.fetch ?? globalThis.fetch; const fetch_ = options.fetch ?? globalThis.fetch;
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
}); });
validateManifest(manifest, target); validateManifest(manifest, target);
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack")); const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null); const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
const reusable = active?.releaseMatches ? active : null;
if (!browserChoice && active?.browserChoice) {
browserChoice = await resolveBrowserChoice(active.browserChoice, {
platform,
env: options.env,
homeDir: options.homeDir,
});
}
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
throw bootstrapError(
"The active browser capability does not record a reusable browser provider; preview browser setup options before changing this runtime.",
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
);
}
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice); const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice);
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`); if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
else printComponentPlan(io.stdout, plan); else printComponentPlan(io.stdout, plan);
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
return applyBrowserProviderToComponents([...selected], browserChoice); return applyBrowserProviderToComponents([...selected], browserChoice);
} }
function mergeRetainedCapabilities(requested, reusable, browserChoice) {
const selected = new Set([
...(Array.isArray(reusable?.selectedCapabilities) ? reusable.selectedCapabilities : []),
...requested,
]);
if (browserChoice?.provider === "installed") selected.delete("browser-visible");
const pending = [...selected];
while (pending.length) {
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
if (!selected.has(dependency)) {
selected.add(dependency);
pending.push(dependency);
}
}
}
return [...selected].sort();
}
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) { function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
const components = selectedComponents(capabilities, browserChoice); const components = selectedComponents(capabilities, browserChoice);
const retained = new Set(reusable?.components ?? []); const retained = new Set(reusable?.components ?? []);
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
const stat = await fs.lstat(root); const stat = await fs.lstat(root);
if (!stat.isDirectory() || stat.isSymbolicLink()) return null; if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8")); const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) || const releaseMatches = bundle?.version === version ||
(typeof bundle?.version === "string" && bundle.version.startsWith(`${version}-caps-`));
if (bundle?.schemaVersion !== 2 || typeof bundle.version !== "string" ||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(bundle.version) || !Array.isArray(bundle.runtimeComponents) ||
!Array.isArray(bundle.files)) return null; !Array.isArray(bundle.files)) return null;
const components = [...new Set(bundle.runtimeComponents)]; const components = [...new Set(bundle.runtimeComponents)];
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null; if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
const selectedCapabilities = Array.isArray(bundle.selectedCapabilities)
? [...new Set(bundle.selectedCapabilities)]
: [];
if (selectedCapabilities.some((capability) => !CAPABILITIES.has(capability))) return null;
let browserChoice = null;
if (browserChoiceRequired(selectedCapabilities)) {
const explicit = bundle.browserChoice;
if (!explicit || !["managed", "installed"].includes(explicit.provider)) return null;
if (explicit.provider === "installed") {
if (selectedCapabilities.includes("browser-visible") ||
typeof explicit.executablePath !== "string" || !path.isAbsolute(explicit.executablePath) ||
components.includes("browser-headless") || components.includes("browser-visible")) return null;
browserChoice = { provider: "installed", executablePath: explicit.executablePath };
} else {
if (!components.includes("browser-headless") && !components.includes("browser-visible")) return null;
browserChoice = { provider: "managed", executablePath: null };
}
}
await assertNoLinks(root); await assertNoLinks(root);
const files = []; const files = [];
const seen = new Set(); const seen = new Set();
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
await sha256File(file) !== entry.sha256) return null; await sha256File(file) !== entry.sha256) return null;
files.push(relative); files.push(relative);
} }
return { root, components, files }; return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
} }
async function seedReusableRuntime(reusable, destination, claimedFiles) { async function seedReusableRuntime(reusable, destination, claimedFiles) {
@@ -496,6 +574,7 @@ async function installFromSource(source, parsed, options) {
if (parsed.home) args.push("--home", path.resolve(parsed.home)); if (parsed.home) args.push("--home", path.resolve(parsed.home));
if (options.version) args.push("--version", options.version); if (options.version) args.push("--version", options.version);
if (options.prepared) args.push("--prepared"); if (options.prepared) args.push("--prepared");
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
await run(options.nodeCommand ?? process.execPath, args); await run(options.nodeCommand ?? process.execPath, args);
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`); options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
return 0; return 0;
@@ -123,13 +123,36 @@ export async function main(argv = process.argv.slice(2), options = {}) {
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE"); throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
} }
if (parsed.source) { if (parsed.source) {
const sourceHome = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const active = await inspectReusableRuntime(sourceHome, BOOTSTRAP_RUNTIME_VERSION).catch(() => null);
if (!browserChoice && active?.browserChoice) {
browserChoice = await resolveBrowserChoice(active.browserChoice, {
platform,
env: options.env,
homeDir: options.homeDir,
});
}
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
throw bootstrapError(
"The active browser capability does not record a reusable browser provider; choose a browser provider before changing this runtime.",
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
);
}
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
if (parsed.action === "preview") { if (parsed.action === "preview") {
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n"); io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
return 0; return 0;
} }
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED"); if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n"); io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false, browserChoice }); return await installFromSource(parsed.source, parsed, {
...options,
...io,
prepared: false,
replaceCapabilities: true,
browserChoice,
});
} }
const fetch_ = options.fetch ?? globalThis.fetch; const fetch_ = options.fetch ?? globalThis.fetch;
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
}); });
validateManifest(manifest, target); validateManifest(manifest, target);
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack")); const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null); const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
const reusable = active?.releaseMatches ? active : null;
if (!browserChoice && active?.browserChoice) {
browserChoice = await resolveBrowserChoice(active.browserChoice, {
platform,
env: options.env,
homeDir: options.homeDir,
});
}
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
throw bootstrapError(
"The active browser capability does not record a reusable browser provider; preview browser setup options before changing this runtime.",
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
);
}
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice); const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice);
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`); if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
else printComponentPlan(io.stdout, plan); else printComponentPlan(io.stdout, plan);
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
return applyBrowserProviderToComponents([...selected], browserChoice); return applyBrowserProviderToComponents([...selected], browserChoice);
} }
function mergeRetainedCapabilities(requested, reusable, browserChoice) {
const selected = new Set([
...(Array.isArray(reusable?.selectedCapabilities) ? reusable.selectedCapabilities : []),
...requested,
]);
if (browserChoice?.provider === "installed") selected.delete("browser-visible");
const pending = [...selected];
while (pending.length) {
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
if (!selected.has(dependency)) {
selected.add(dependency);
pending.push(dependency);
}
}
}
return [...selected].sort();
}
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) { function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
const components = selectedComponents(capabilities, browserChoice); const components = selectedComponents(capabilities, browserChoice);
const retained = new Set(reusable?.components ?? []); const retained = new Set(reusable?.components ?? []);
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
const stat = await fs.lstat(root); const stat = await fs.lstat(root);
if (!stat.isDirectory() || stat.isSymbolicLink()) return null; if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8")); const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) || const releaseMatches = bundle?.version === version ||
(typeof bundle?.version === "string" && bundle.version.startsWith(`${version}-caps-`));
if (bundle?.schemaVersion !== 2 || typeof bundle.version !== "string" ||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(bundle.version) || !Array.isArray(bundle.runtimeComponents) ||
!Array.isArray(bundle.files)) return null; !Array.isArray(bundle.files)) return null;
const components = [...new Set(bundle.runtimeComponents)]; const components = [...new Set(bundle.runtimeComponents)];
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null; if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
const selectedCapabilities = Array.isArray(bundle.selectedCapabilities)
? [...new Set(bundle.selectedCapabilities)]
: [];
if (selectedCapabilities.some((capability) => !CAPABILITIES.has(capability))) return null;
let browserChoice = null;
if (browserChoiceRequired(selectedCapabilities)) {
const explicit = bundle.browserChoice;
if (!explicit || !["managed", "installed"].includes(explicit.provider)) return null;
if (explicit.provider === "installed") {
if (selectedCapabilities.includes("browser-visible") ||
typeof explicit.executablePath !== "string" || !path.isAbsolute(explicit.executablePath) ||
components.includes("browser-headless") || components.includes("browser-visible")) return null;
browserChoice = { provider: "installed", executablePath: explicit.executablePath };
} else {
if (!components.includes("browser-headless") && !components.includes("browser-visible")) return null;
browserChoice = { provider: "managed", executablePath: null };
}
}
await assertNoLinks(root); await assertNoLinks(root);
const files = []; const files = [];
const seen = new Set(); const seen = new Set();
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
await sha256File(file) !== entry.sha256) return null; await sha256File(file) !== entry.sha256) return null;
files.push(relative); files.push(relative);
} }
return { root, components, files }; return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
} }
async function seedReusableRuntime(reusable, destination, claimedFiles) { async function seedReusableRuntime(reusable, destination, claimedFiles) {
@@ -496,6 +574,7 @@ async function installFromSource(source, parsed, options) {
if (parsed.home) args.push("--home", path.resolve(parsed.home)); if (parsed.home) args.push("--home", path.resolve(parsed.home));
if (options.version) args.push("--version", options.version); if (options.version) args.push("--version", options.version);
if (options.prepared) args.push("--prepared"); if (options.prepared) args.push("--prepared");
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
await run(options.nodeCommand ?? process.execPath, args); await run(options.nodeCommand ?? process.execPath, args);
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`); options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
return 0; return 0;
+2 -2
View File
@@ -1,5 +1,5 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. --> <!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
<!-- GSTACK2_PROVENANCE source=canary/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=d1eb2950aba2fa2b09d90f13143492c60d46793c baseline_render_sha256=8dd0ff918566e1c5536f1bfcc546eebbdbee5e43d0a05b14a8efb898d3574dbe ported_render_sha256=89be5f218da2bd812303c87b8c177081727727e5e0d2dc74eb7a73299794d5ef disposition=BUG_FIX --> <!-- GSTACK2_PROVENANCE source=canary/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=d1eb2950aba2fa2b09d90f13143492c60d46793c baseline_render_sha256=8dd0ff918566e1c5536f1bfcc546eebbdbee5e43d0a05b14a8efb898d3574dbe ported_render_sha256=b7f753ba0b98d8c7378dc797dca5950b14ebe26565bac25b5bbaa56b8ea8e13b disposition=BUG_FIX -->
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module canary visibility=primary depth=deep mutation=report-only web=production --> <!-- GSTACK2_ROUTING replacement=$qa --mode Report --module canary visibility=primary depth=deep mutation=report-only web=production -->
<!-- GSTACK2_LEGACY_BODY_START source=canary --> <!-- GSTACK2_LEGACY_BODY_START source=canary -->
@@ -32,7 +32,7 @@ fi
``` ```
If `NEEDS_SETUP`: If `NEEDS_SETUP`:
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait. 1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`. 2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow. 3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
@@ -1,5 +1,5 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. --> <!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
<!-- GSTACK2_PROVENANCE source=land-and-deploy/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=98976ad020d541d251cc7e34802a13458ddc88e2 baseline_render_sha256=be77d9332d68281785eb2daf1d094f53bad537dfa282a4abb8638aca398cd2b9 ported_render_sha256=6920f3d97ce474b8f20c8b3e38ca9d3c03973e47af33103a60bab7c02eb867bd disposition=BUG_FIX --> <!-- GSTACK2_PROVENANCE source=land-and-deploy/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=98976ad020d541d251cc7e34802a13458ddc88e2 baseline_render_sha256=be77d9332d68281785eb2daf1d094f53bad537dfa282a4abb8638aca398cd2b9 ported_render_sha256=405924730c4e328c1a45de26576cda686d0d7da1fc4a36e840458683e1396aa2 disposition=BUG_FIX -->
<!-- GSTACK2_ROUTING replacement=$ship --mode Land --module land-and-deploy visibility=primary depth=deep mutation=merge-deploy web=production --> <!-- GSTACK2_ROUTING replacement=$ship --mode Land --module land-and-deploy visibility=primary depth=deep mutation=merge-deploy web=production -->
<!-- GSTACK2_LEGACY_BODY_START source=land-and-deploy --> <!-- GSTACK2_LEGACY_BODY_START source=land-and-deploy -->
@@ -32,7 +32,7 @@ fi
``` ```
If `NEEDS_SETUP`: If `NEEDS_SETUP`:
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait. 1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`. 2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow. 3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
@@ -123,13 +123,36 @@ export async function main(argv = process.argv.slice(2), options = {}) {
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE"); throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
} }
if (parsed.source) { if (parsed.source) {
const sourceHome = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const active = await inspectReusableRuntime(sourceHome, BOOTSTRAP_RUNTIME_VERSION).catch(() => null);
if (!browserChoice && active?.browserChoice) {
browserChoice = await resolveBrowserChoice(active.browserChoice, {
platform,
env: options.env,
homeDir: options.homeDir,
});
}
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
throw bootstrapError(
"The active browser capability does not record a reusable browser provider; choose a browser provider before changing this runtime.",
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
);
}
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
if (parsed.action === "preview") { if (parsed.action === "preview") {
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n"); io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
return 0; return 0;
} }
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED"); if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n"); io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false, browserChoice }); return await installFromSource(parsed.source, parsed, {
...options,
...io,
prepared: false,
replaceCapabilities: true,
browserChoice,
});
} }
const fetch_ = options.fetch ?? globalThis.fetch; const fetch_ = options.fetch ?? globalThis.fetch;
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
}); });
validateManifest(manifest, target); validateManifest(manifest, target);
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack")); const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null); const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
const reusable = active?.releaseMatches ? active : null;
if (!browserChoice && active?.browserChoice) {
browserChoice = await resolveBrowserChoice(active.browserChoice, {
platform,
env: options.env,
homeDir: options.homeDir,
});
}
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
throw bootstrapError(
"The active browser capability does not record a reusable browser provider; preview browser setup options before changing this runtime.",
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
);
}
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice); const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice);
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`); if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
else printComponentPlan(io.stdout, plan); else printComponentPlan(io.stdout, plan);
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
return applyBrowserProviderToComponents([...selected], browserChoice); return applyBrowserProviderToComponents([...selected], browserChoice);
} }
function mergeRetainedCapabilities(requested, reusable, browserChoice) {
const selected = new Set([
...(Array.isArray(reusable?.selectedCapabilities) ? reusable.selectedCapabilities : []),
...requested,
]);
if (browserChoice?.provider === "installed") selected.delete("browser-visible");
const pending = [...selected];
while (pending.length) {
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
if (!selected.has(dependency)) {
selected.add(dependency);
pending.push(dependency);
}
}
}
return [...selected].sort();
}
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) { function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
const components = selectedComponents(capabilities, browserChoice); const components = selectedComponents(capabilities, browserChoice);
const retained = new Set(reusable?.components ?? []); const retained = new Set(reusable?.components ?? []);
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
const stat = await fs.lstat(root); const stat = await fs.lstat(root);
if (!stat.isDirectory() || stat.isSymbolicLink()) return null; if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8")); const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) || const releaseMatches = bundle?.version === version ||
(typeof bundle?.version === "string" && bundle.version.startsWith(`${version}-caps-`));
if (bundle?.schemaVersion !== 2 || typeof bundle.version !== "string" ||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(bundle.version) || !Array.isArray(bundle.runtimeComponents) ||
!Array.isArray(bundle.files)) return null; !Array.isArray(bundle.files)) return null;
const components = [...new Set(bundle.runtimeComponents)]; const components = [...new Set(bundle.runtimeComponents)];
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null; if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
const selectedCapabilities = Array.isArray(bundle.selectedCapabilities)
? [...new Set(bundle.selectedCapabilities)]
: [];
if (selectedCapabilities.some((capability) => !CAPABILITIES.has(capability))) return null;
let browserChoice = null;
if (browserChoiceRequired(selectedCapabilities)) {
const explicit = bundle.browserChoice;
if (!explicit || !["managed", "installed"].includes(explicit.provider)) return null;
if (explicit.provider === "installed") {
if (selectedCapabilities.includes("browser-visible") ||
typeof explicit.executablePath !== "string" || !path.isAbsolute(explicit.executablePath) ||
components.includes("browser-headless") || components.includes("browser-visible")) return null;
browserChoice = { provider: "installed", executablePath: explicit.executablePath };
} else {
if (!components.includes("browser-headless") && !components.includes("browser-visible")) return null;
browserChoice = { provider: "managed", executablePath: null };
}
}
await assertNoLinks(root); await assertNoLinks(root);
const files = []; const files = [];
const seen = new Set(); const seen = new Set();
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
await sha256File(file) !== entry.sha256) return null; await sha256File(file) !== entry.sha256) return null;
files.push(relative); files.push(relative);
} }
return { root, components, files }; return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
} }
async function seedReusableRuntime(reusable, destination, claimedFiles) { async function seedReusableRuntime(reusable, destination, claimedFiles) {
@@ -496,6 +574,7 @@ async function installFromSource(source, parsed, options) {
if (parsed.home) args.push("--home", path.resolve(parsed.home)); if (parsed.home) args.push("--home", path.resolve(parsed.home));
if (options.version) args.push("--version", options.version); if (options.version) args.push("--version", options.version);
if (options.prepared) args.push("--prepared"); if (options.prepared) args.push("--prepared");
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
await run(options.nodeCommand ?? process.execPath, args); await run(options.nodeCommand ?? process.execPath, args);
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`); options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
return 0; return 0;
+61
View File
@@ -161,6 +161,23 @@ describe("GStack 2 managed runtime installer", () => {
}); });
}); });
test("managed headless launchers require the separately approved visible-browser slot", async () => {
await withFixture(async ({ source, home }) => {
await installFixture(source, home, "managed-visible-refusal", {
entries: BROWSER_ENTRIES,
capabilities: BROWSER_CAPABILITIES,
browserChoice: { provider: "managed", executablePath: null },
});
await configSetBrowserChoice(home, { provider: "managed", executablePath: null });
await expect(runInstalledLauncher(home, "browse", ["--headed"], { capture: true }))
.rejects.toMatchObject({ stderr: expect.stringContaining("does not include visible Chromium") });
await expect(runInstalledLauncher(home, "browse", ["connect"], { capture: true }))
.rejects.toMatchObject({ stderr: expect.stringContaining("does not include visible Chromium") });
await expect(runInstalledLauncher(home, "browse", ["handoff"], { capture: true }))
.rejects.toMatchObject({ stderr: expect.stringContaining("does not include visible Chromium") });
});
});
test("design-only launchers do not require an unrelated browser selection", async () => { test("design-only launchers do not require an unrelated browser selection", async () => {
await withFixture(async ({ source, home }) => { await withFixture(async ({ source, home }) => {
const design = path.join(source, "design", "dist", "design"); const design = path.join(source, "design", "dist", "design");
@@ -248,6 +265,45 @@ describe("GStack 2 managed runtime installer", () => {
}); });
}); });
test("rollback switches the runtime pointer and recorded browser choice in one recoverable transaction", async () => {
await withFixture(async ({ source, home }) => {
const executable = await fs.realpath(process.execPath);
const fallback = await installFixture(source, home, "installed-fallback-valid");
const fallbackManifestPath = path.join(fallback.path, ".gstack-bundle.json");
const fallbackManifest = await readJson(fallbackManifestPath);
await fs.writeFile(fallbackManifestPath, JSON.stringify({
...fallbackManifest,
selectedCapabilities: ["browser"],
runtimeComponents: ["browser-code", "core"],
browserChoice: { provider: "installed", executablePath: executable },
}));
const current = await installFixture(source, home, "managed-current-valid");
const currentManifestPath = path.join(current.path, ".gstack-bundle.json");
const currentManifest = await readJson(currentManifestPath);
await fs.writeFile(currentManifestPath, JSON.stringify({
...currentManifest,
selectedCapabilities: ["browser"],
runtimeComponents: ["browser-headless", "core"],
browserChoice: { provider: "managed", executablePath: null },
}));
await configSetBrowserChoice(home, { provider: "managed", executablePath: null });
const output = captureStream();
expect(await runtimeMain(["upgrade", "--rollback"], {
cwd: source,
env: { ...process.env, GSTACK_HOME: home },
stdout: output.stream,
stderr: output.stream,
})).toBe(0);
expect(await readJson(path.join(home, "versions", "current.json")))
.toMatchObject({ current: "installed-fallback-valid", lastKnownGood: "managed-current-valid" });
expect((await readJson(path.join(home, "config.json"))).browser)
.toEqual({ provider: "installed", executablePath: executable });
expect(await exists(path.join(home, ".gstack-runtime-transaction.json"))).toBe(false);
});
});
test("an installed-browser setup persists the choice only after activation and launches through it", async () => { test("an installed-browser setup persists the choice only after activation and launches through it", async () => {
await withFixture(async ({ root, source, home }) => { await withFixture(async ({ root, source, home }) => {
await fs.writeFile(path.join(source, "cap", "tool"), `#!/usr/bin/env node await fs.writeFile(path.join(source, "cap", "tool"), `#!/usr/bin/env node
@@ -958,14 +1014,17 @@ process.stdout.write(process.env.GSTACK_CHROMIUM_PATH || "unset");
test("a launcher repairs a crash journal before resolving any runtime", async () => { test("a launcher repairs a crash journal before resolving any runtime", async () => {
await withFixture(async ({ source, home }) => { await withFixture(async ({ source, home }) => {
await installFixture(source, home, "1.0.0"); await installFixture(source, home, "1.0.0");
await configSetBrowserChoice(home, { provider: "managed", executablePath: null });
const pointer = await readJson(path.join(home, "versions", "current.json")); const pointer = await readJson(path.join(home, "versions", "current.json"));
const manifest = await fs.readFile(path.join(home, "runtime-install.json")); const manifest = await fs.readFile(path.join(home, "runtime-install.json"));
const config = await fs.readFile(path.join(home, "config.json"));
// Keep the launcher for this host executable so it can enter the shared // Keep the launcher for this host executable so it can enter the shared
// recovery path. The transaction restores the inactive host variant. // recovery path. The transaction restores the inactive host variant.
const recoverableLauncher = process.platform === "win32" ? "gstack" : "gstack.cmd"; const recoverableLauncher = process.platform === "win32" ? "gstack" : "gstack.cmd";
const launcherPath = path.join(home, "bin", recoverableLauncher); const launcherPath = path.join(home, "bin", recoverableLauncher);
const launcherBefore = await fs.readFile(launcherPath); const launcherBefore = await fs.readFile(launcherPath);
await fs.writeFile(path.join(home, "runtime-install.json"), '{"activeVersion":"crashed"}\n'); await fs.writeFile(path.join(home, "runtime-install.json"), '{"activeVersion":"crashed"}\n');
await configSetBrowserChoice(home, { provider: "installed", executablePath: process.execPath });
await fs.writeFile(launcherPath, "candidate launcher\n"); await fs.writeFile(launcherPath, "candidate launcher\n");
await fs.writeFile(path.join(home, "versions", "current.json"), `${JSON.stringify({ await fs.writeFile(path.join(home, "versions", "current.json"), `${JSON.stringify({
schemaVersion: 2, schemaVersion: 2,
@@ -982,6 +1041,7 @@ process.stdout.write(process.env.GSTACK_CHROMIUM_PATH || "unset");
previousPointerExists: true, previousPointerExists: true,
previousPointer: pointer, previousPointer: pointer,
files: [ files: [
{ path: "config.json", existed: true, mode: 0o644, dataBase64: config.toString("base64") },
{ path: "runtime-install.json", existed: true, mode: 0o600, dataBase64: manifest.toString("base64") }, { path: "runtime-install.json", existed: true, mode: 0o600, dataBase64: manifest.toString("base64") },
{ path: `bin/${recoverableLauncher}`, existed: true, mode: 0o644, dataBase64: launcherBefore.toString("base64") }, { path: `bin/${recoverableLauncher}`, existed: true, mode: 0o644, dataBase64: launcherBefore.toString("base64") },
], ],
@@ -997,6 +1057,7 @@ process.stdout.write(process.env.GSTACK_CHROMIUM_PATH || "unset");
const launched = await runInstalledLauncher(home, "gstack", ["doctor"], { capture: true }); const launched = await runInstalledLauncher(home, "gstack", ["doctor"], { capture: true });
expect(launched.stdout).toContain("gstack fixture doctor"); expect(launched.stdout).toContain("gstack fixture doctor");
expect(await readJson(path.join(home, "versions", "current.json"))).toEqual(pointer); expect(await readJson(path.join(home, "versions", "current.json"))).toEqual(pointer);
expect(await fs.readFile(path.join(home, "config.json"))).toEqual(config);
expect(await fs.readFile(path.join(home, "runtime-install.json"))).toEqual(manifest); expect(await fs.readFile(path.join(home, "runtime-install.json"))).toEqual(manifest);
expect(await fs.readFile(launcherPath)).toEqual(launcherBefore); expect(await fs.readFile(launcherPath)).toEqual(launcherBefore);
expect(await exists(path.join(home, ".gstack-runtime-transaction.json"))).toBe(false); expect(await exists(path.join(home, ".gstack-runtime-transaction.json"))).toBe(false);
@@ -249,6 +249,25 @@ describe("one config authority", () => {
expect(setup.stdout).toContain("optional runtime: unchanged"); expect(setup.stdout).toContain("optional runtime: unchanged");
}); });
test("generic config writes cannot create an incoherent browser selection", async () => {
const base = await root();
const home = path.join(base, "state");
const project = path.join(base, "project");
await fs.mkdir(project);
const run = (args: string[]) => spawnSync(process.execPath, [gstackBin, ...args], {
cwd: project,
encoding: "utf8",
env: { ...process.env, GSTACK_HOME: home },
});
for (const key of ["browser", "browser.provider", "browser.executablePath"]) {
const result = run(["config", "set", key, "installed"]);
expect(result.status).toBe(1);
expect(result.stderr).toContain("gstack config browser");
}
expect(await fs.stat(path.join(home, "config.json")).catch(() => null)).toBeNull();
});
test("legacy YAML is read-only migration input and JSON takes authority on write", async () => { test("legacy YAML is read-only migration input and JSON takes authority on write", async () => {
const home = path.join(await root(), "legacy"); const home = path.join(await root(), "legacy");
await fs.mkdir(home); await fs.mkdir(home);
+205
View File
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { spawnSync } from "node:child_process"; import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { PassThrough, Readable } from "node:stream"; import { PassThrough, Readable } from "node:stream";
import { runDoctor } from "../runtime/doctor.js"; import { runDoctor } from "../runtime/doctor.js";
import { runInstallerCli, runtimeSlotVersion, runtimeSurfaceForCapabilities } from "../runtime/install.js"; import { runInstallerCli, runtimeSlotVersion, runtimeSurfaceForCapabilities } from "../runtime/install.js";
@@ -49,6 +50,36 @@ function officialManifestFixture(target: string, customize?: (component: string,
}; };
} }
async function createActiveRuntimeFixture(home: string, options: {
bundleVersion: string;
selectedCapabilities: string[];
runtimeComponents: string[];
browserChoice: { provider: "managed" | "installed"; executablePath: string | null };
}) {
const root = path.join(home, "versions", "active-slot");
const payload = Buffer.from("verified active runtime payload\n");
await fs.mkdir(root, { recursive: true });
await fs.writeFile(path.join(root, "payload.txt"), payload);
await fs.writeFile(path.join(root, ".gstack-bundle.json"), JSON.stringify({
schemaVersion: 2,
version: options.bundleVersion,
selectedCapabilities: options.selectedCapabilities,
runtimeComponents: options.runtimeComponents,
browserChoice: options.browserChoice,
files: [{
path: "payload.txt",
size: payload.byteLength,
sha256: createHash("sha256").update(payload).digest("hex"),
}],
}));
await fs.writeFile(path.join(home, "versions", "current.json"), JSON.stringify({
schemaVersion: 2,
status: "active",
current: "active-slot",
lastKnownGood: null,
}));
}
describe("GStack runtime setup UX", () => { describe("GStack runtime setup UX", () => {
test("capability selection keeps the core and excludes unselected heavyweight surfaces", () => { test("capability selection keeps the core and excludes unselected heavyweight surfaces", () => {
const surface = runtimeSurfaceForCapabilities(["browser"]); const surface = runtimeSurfaceForCapabilities(["browser"]);
@@ -277,6 +308,32 @@ describe("GStack runtime setup UX", () => {
} }
}); });
test("explicit install-later needs no browser selection and does not prompt or mutate", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-browser-later-"));
try {
const home = path.join(root, "home");
const output = capture();
const input = Readable.from([]) as Readable & { isTTY: boolean };
input.isTTY = false;
expect(await runInstallerCli([
"--source", path.resolve(import.meta.dir, ".."),
"--home", home,
"--capabilities", "browser",
"--install-later",
"--json",
], { stdin: input, stdout: output.stream, stderr: output.stream })).toBe(0);
expect(JSON.parse(output.value())).toMatchObject({
ok: true,
action: "install-later",
mutated: false,
preview: null,
});
await expect(fs.stat(home)).rejects.toMatchObject({ code: "ENOENT" });
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
test("browser bootstrap options are local-only and browser preview requires an explicit choice", async () => { test("browser bootstrap options are local-only and browser preview requires an explicit choice", async () => {
const output = capture(); const output = capture();
let fetches = 0; let fetches = 0;
@@ -332,6 +389,76 @@ describe("GStack runtime setup UX", () => {
expect(fetches).toBe(1); expect(fetches).toBe(1);
}); });
test("official previews retain an active installed-browser choice across same- and cross-release additions", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-bootstrap-retain-browser-"));
const executable = await fs.realpath(process.execPath);
const target = `${process.platform === "win32" ? "windows" : process.platform}-${process.arch}`;
try {
for (const [name, bundleVersion, expectsReuse] of [
["same", `${BOOTSTRAP_RUNTIME_VERSION}-caps-installed`, true],
["cross", "1.9.0-caps-installed", false],
] as const) {
const home = path.join(root, name);
await createActiveRuntimeFixture(home, {
bundleVersion,
selectedCapabilities: ["browser"],
runtimeComponents: ["browser-code", "core"],
browserChoice: { provider: "installed", executablePath: executable },
});
const output = capture();
expect(await bootstrapMain([
"preview", "--capability", "design", "--home", home, "--json",
], {
stdout: output.stream,
stderr: output.stream,
libc: process.platform === "linux" ? "glibc" : undefined,
fetch: async (url: string) => ({ ok: true, url, json: async () => officialManifestFixture(target) }),
})).toBe(0);
const result = JSON.parse(output.value());
expect(result.capabilities).toEqual(["browser", "design"]);
expect(result.browser).toEqual({ provider: "installed", executablePath: executable });
expect(result.components).toEqual(["browser-code", "core", "design"]);
expect(result.components).not.toContain("browser-headless");
expect(result.reusedComponents.length > 0).toBe(expectsReuse);
}
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
test("switching a reusable managed visible slot to installed drops visible payload from the exact plan", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-bootstrap-provider-switch-"));
const home = path.join(root, "home");
const executable = await fs.realpath(process.execPath);
const target = `${process.platform === "win32" ? "windows" : process.platform}-${process.arch}`;
try {
await createActiveRuntimeFixture(home, {
bundleVersion: `${BOOTSTRAP_RUNTIME_VERSION}-caps-managed-visible`,
selectedCapabilities: ["browser-visible"],
runtimeComponents: ["browser-code", "browser-visible", "core"],
browserChoice: { provider: "managed", executablePath: null },
});
const output = capture();
expect(await bootstrapMain([
"preview", "--capability", "browser", "--browser", "installed",
"--browser-path", executable, "--home", home, "--json",
], {
stdout: output.stream,
stderr: output.stream,
libc: process.platform === "linux" ? "glibc" : undefined,
fetch: async (url: string) => ({ ok: true, url, json: async () => officialManifestFixture(target) }),
})).toBe(0);
const result = JSON.parse(output.value());
expect(result.capabilities).toEqual(["browser"]);
expect(result.browser.provider).toBe("installed");
expect(result.components).toEqual(["browser-code", "core"]);
expect(result.components).not.toContain("browser-visible");
expect(result.components).not.toContain("browser-headless");
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
test("visible GStack Browser refuses installed Chrome before any network request", async () => { test("visible GStack Browser refuses installed Chrome before any network request", async () => {
const output = capture(); const output = capture();
let fetches = 0; let fetches = 0;
@@ -468,6 +595,43 @@ describe("GStack runtime setup UX", () => {
} }
}); });
test("doctor reports and launches an internal managed visible-browser slot", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-doctor-visible-browser-"));
const home = path.join(root, "home");
try {
await setupRuntime({ home, cwd: root });
const paths = resolveRuntimePaths({ home });
const active = path.join(paths.versions, "fixture");
const browserRoot = path.join(active, ".gstack-runtime-browsers");
const managedBun = path.join(active, ".gstack-runtime-tools", process.platform === "win32" ? "bun.exe" : "bun");
const playwright = path.join(active, "node_modules", "playwright");
await fs.mkdir(path.join(browserRoot, "chromium-fixture"), { recursive: true });
await fs.mkdir(path.dirname(managedBun), { recursive: true });
await fs.mkdir(playwright, { recursive: true });
await fs.copyFile(process.execPath, managedBun);
if (process.platform !== "win32") await fs.chmod(managedBun, 0o755);
await fs.writeFile(path.join(playwright, "index.mjs"),
`export const chromium = { launch: async ({ headless, channel }) => { if (headless !== true || channel !== "chromium") throw new Error("expected full Chromium channel"); return { version: () => "fixture-visible", close: async () => {} }; } };\n`);
await fs.writeFile(path.join(active, ".gstack-bundle.json"), JSON.stringify({
compatibility: { skillApi: "2.0" },
selectedCapabilities: ["browser-visible"],
capabilities: { browse: "browse/dist/browse" },
tools: { bun: { path: path.relative(active, managedBun).split(path.sep).join("/"), version: "1.3.14" } },
}));
await fs.writeFile(paths.versionPointer, JSON.stringify({
schemaVersion: 2, status: "active", current: "fixture", lastKnownGood: "fixture",
}));
await configSetBrowserChoice(home, { provider: "managed", executablePath: null });
const report = await runDoctor({ home, cwd: root, nodeCommand: process.execPath });
expect(report.checks.find((check) => check.id === "capability:browser-visible")).toMatchObject({
status: "pass",
details: { browserRoot, version: "fixture-visible" },
});
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
test("doctor launches the explicitly selected installed browser through the same Playwright adapter", async () => { test("doctor launches the explicitly selected installed browser through the same Playwright adapter", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-doctor-installed-browser-")); const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-doctor-installed-browser-"));
const home = path.join(root, "home"); const home = path.join(root, "home");
@@ -660,9 +824,50 @@ describe("GStack runtime setup UX", () => {
expect(args).toContain("--yes"); expect(args).toContain("--yes");
expect(args).toContain("browser,diagram,pdf"); expect(args).toContain("browser,diagram,pdf");
expect(args).not.toContain("--prepared"); expect(args).not.toContain("--prepared");
expect(args).toContain("--replace-capabilities");
expect(output.value()).toContain("Developer-only source install"); expect(output.value()).toContain("Developer-only source install");
} finally { } finally {
await fs.rm(root, { recursive: true, force: true }); await fs.rm(root, { recursive: true, force: true });
} }
}); });
test("developer source fallback can switch a retained managed visible slot to installed", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-bootstrap-source-switch-"));
const source = path.join(root, "source");
const runtime = path.join(source, "runtime");
const home = path.join(root, "home");
const log = path.join(root, "args.json");
const executable = await fs.realpath(process.execPath);
const output = capture();
try {
await fs.mkdir(runtime, { recursive: true });
await fs.writeFile(path.join(runtime, "install.js"),
`import fs from "node:fs"; fs.writeFileSync(process.env.BOOTSTRAP_TEST_LOG, JSON.stringify(process.argv.slice(2)));\n`);
await createActiveRuntimeFixture(home, {
bundleVersion: `${BOOTSTRAP_RUNTIME_VERSION}-caps-managed-visible`,
selectedCapabilities: ["browser-visible"],
runtimeComponents: ["browser-code", "browser-visible", "core"],
browserChoice: { provider: "managed", executablePath: null },
});
const previous = process.env.BOOTSTRAP_TEST_LOG;
process.env.BOOTSTRAP_TEST_LOG = log;
try {
expect(await bootstrapMain([
"install", "--source", source, "--capability", "browser", "--browser", "installed",
"--browser-path", executable, "--home", home, "--yes",
], { stdout: output.stream, stderr: output.stream })).toBe(0);
} finally {
if (previous == null) delete process.env.BOOTSTRAP_TEST_LOG;
else process.env.BOOTSTRAP_TEST_LOG = previous;
}
const args = JSON.parse(await fs.readFile(log, "utf8"));
expect(args).toContain("browser");
expect(args).toContain("installed");
expect(args).toContain(executable);
expect(args).toContain("--replace-capabilities");
expect(args).not.toContain("browser-visible");
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
}); });