mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-21 20:30:47 +02:00
fix: harden browser provider activation
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!-- 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_LEGACY_BODY_START source=canary -->
|
||||
@@ -32,7 +32,7 @@ fi
|
||||
```
|
||||
|
||||
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`.
|
||||
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. -->
|
||||
<!-- 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_LEGACY_BODY_START source=land-and-deploy -->
|
||||
@@ -32,7 +32,7 @@ fi
|
||||
```
|
||||
|
||||
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`.
|
||||
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");
|
||||
}
|
||||
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") {
|
||||
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;
|
||||
}
|
||||
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");
|
||||
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;
|
||||
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
});
|
||||
validateManifest(manifest, target);
|
||||
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);
|
||||
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
|
||||
else printComponentPlan(io.stdout, plan);
|
||||
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, 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) {
|
||||
const components = selectedComponents(capabilities, browserChoice);
|
||||
const retained = new Set(reusable?.components ?? []);
|
||||
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
|
||||
const stat = await fs.lstat(root);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||
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;
|
||||
const components = [...new Set(bundle.runtimeComponents)];
|
||||
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);
|
||||
const files = [];
|
||||
const seen = new Set();
|
||||
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
||||
await sha256File(file) !== entry.sha256) return null;
|
||||
files.push(relative);
|
||||
}
|
||||
return { root, components, files };
|
||||
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||
}
|
||||
|
||||
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 (options.version) args.push("--version", options.version);
|
||||
if (options.prepared) args.push("--prepared");
|
||||
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||
await run(options.nodeCommand ?? process.execPath, args);
|
||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user