mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-15 01:15:29 +02:00
feat(third-party-actions): Aside is the recommended driver; gstack's visible browser stays the fallback
The readiness probe is lifted from {{ASIDE_SETUP}} at gen time (byte-identity pinned) and rule 3 points at browse/SKILL.md for how to drive; the consent question offers Aside first and gstack's own visible browser (handoff/resume for sign-in) as the fallback, as v1.72 framed it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,20 @@
|
||||
/**
|
||||
* Consent-gate E2E for the Third-Party Web Actions contract (gate tier).
|
||||
*
|
||||
* The contract's behavior — offer the Aside drive when detected, degrade to
|
||||
* the first-party stack when absent, pitch the download exactly once on
|
||||
* macOS only, and NEVER offer a browser drive for Apple credential work —
|
||||
* is prose, so wording pins alone can't prove an agent follows it. These
|
||||
* five cases run the real contract section through `claude -p` in the
|
||||
* hermetic clean room with PATH shims controlling what "installed" means:
|
||||
* The contract's behavior — offer the Aside drive first when detected, fall
|
||||
* back to gstack's own visible browser (`$B` headed + handoff) / manual steps
|
||||
* / defer when absent, pitch the download exactly once on macOS only, and
|
||||
* NEVER offer a browser drive for Apple credential work — is prose, so
|
||||
* wording pins alone can't prove an agent follows it. These five cases run
|
||||
* the real contract section through `claude -p` in the hermetic clean room
|
||||
* with PATH shims controlling what "installed" means:
|
||||
*
|
||||
* tpa-present → consent question offers the Aside drive
|
||||
* tpa-absent-linux → first-party offer, zero download pitch
|
||||
* tpa-broken → present-but-broken CLI behaves exactly like absent
|
||||
* tpa-present → consent question offers the Aside drive (recommended)
|
||||
* alongside the gstack drive
|
||||
* tpa-absent-linux → gstack drive / manual / defer offer, zero download pitch
|
||||
* tpa-broken → present-but-not-running CLI: "open the Aside app" ask
|
||||
* or the gstack drive / manual / defer question — never
|
||||
* an Aside drive offer
|
||||
* tpa-absent-darwin → aside.com pitch exactly once, names macOS 15+
|
||||
* tpa-apple-ban → ZERO drive offers for an app-specific password
|
||||
* (the fork shipped this exact incident once; never again)
|
||||
@@ -47,7 +51,7 @@ function contractSection(): string {
|
||||
}
|
||||
|
||||
interface ShimSpec {
|
||||
/** aside shim behavior: 'ok' answers --version/--help, 'broken' exits 1, 'absent' = no shim. */
|
||||
/** aside shim behavior: 'ok' answers the repl readiness probe + --version/--help, 'broken' exits 1, 'absent' = no shim. */
|
||||
aside: 'ok' | 'broken' | 'absent';
|
||||
/** What the shimmed `uname` prints (deterministic across dev/CI platforms). */
|
||||
uname: 'Darwin' | 'Linux';
|
||||
@@ -61,7 +65,7 @@ function setupCase(spec: ShimSpec, extraDocs: Record<string, string> = {}) {
|
||||
|
||||
if (spec.aside !== 'absent') {
|
||||
const body = spec.aside === 'ok'
|
||||
? '#!/bin/sh\ncase "$1" in\n --version) echo "aside 1.26.810.1915"; exit 0 ;;\n --help) echo "usage: aside [exec|repl|mcp] ..."; exit 0 ;;\n *) echo "aside: daemon not reachable — make sure Aside Browser is running" >&2; exit 1 ;;\nesac\n'
|
||||
? '#!/bin/sh\ncase "$1" in\n --version) echo "aside 1.26.810.1915"; exit 0 ;;\n --help) echo "usage: aside [exec|repl|mcp] ..."; exit 0 ;;\n repl) echo "ASIDE_READY /tmp/aside-shim-session"; exit 0 ;;\n *) echo "aside: daemon not reachable — make sure Aside Browser is running" >&2; exit 1 ;;\nesac\n'
|
||||
: '#!/bin/sh\necho "aside: daemon not reachable — make sure Aside Browser is running" >&2\nexit 1\n';
|
||||
fs.writeFileSync(path.join(shimDir, 'aside'), body, { mode: 0o755 });
|
||||
}
|
||||
@@ -156,7 +160,7 @@ describeIfSelected('third-party-actions consent gate', TPA_TESTS, () => {
|
||||
} finally { cleanup(); }
|
||||
}, 6 * 60_000);
|
||||
|
||||
// aside absent on Linux → first-party offer, ZERO download pitch.
|
||||
// aside absent on Linux → gstack drive / manual / defer, ZERO download pitch.
|
||||
testIfSelected('tpa-absent-linux', async () => {
|
||||
const { workDir, env, cleanup } = setupCase({ aside: 'absent', uname: 'Linux' });
|
||||
try {
|
||||
@@ -175,11 +179,15 @@ describeIfSelected('third-party-actions consent gate', TPA_TESTS, () => {
|
||||
// keep the contract's B/C/D lettering with A dropped (observed live).
|
||||
expect(text).toMatch(/\b[A-D]\)/);
|
||||
expect(text).toMatch(/manual/i);
|
||||
// The gstack drive is the universal fallback — it must be on offer.
|
||||
expect(text).toMatch(/gstack('s| own| drive)|\$B|headed|visible browser/i);
|
||||
} finally { cleanup(); }
|
||||
}, 6 * 60_000);
|
||||
|
||||
// aside present but broken (daemon down at probe time) → behaves exactly
|
||||
// like absent: no Aside drive offer.
|
||||
// aside present but not running (the repl readiness probe fails) → the
|
||||
// contract asks the user to open the Aside app and re-probes once, THEN
|
||||
// treats Aside as not detected (gstack drive / manual / defer). Never an
|
||||
// Aside drive offer.
|
||||
testIfSelected('tpa-broken', async () => {
|
||||
const { workDir, env, cleanup } = setupCase({ aside: 'broken', uname: 'Linux' });
|
||||
try {
|
||||
@@ -191,11 +199,15 @@ describeIfSelected('third-party-actions consent gate', TPA_TESTS, () => {
|
||||
recordE2E(evalCollector, 'tpa-broken', 'e2e-third-party-actions', result);
|
||||
expect(result.exitReason).toBe('success');
|
||||
const text = assistantText(result.transcript);
|
||||
expect(text).not.toMatch(/in your Aside browser/i);
|
||||
// Lettered consent question; broken-daemon renderings legitimately keep
|
||||
// the contract's B/C/D lettering with the Aside option dropped
|
||||
// (observed live), so accept any option letter.
|
||||
expect(text).toMatch(/\b[A-D]\)/);
|
||||
expect(text).not.toMatch(/in your Aside browser/i); // load-bearing negative
|
||||
// Either outcome the contract permits in one-shot `claude -p`: the
|
||||
// "open the Aside app" ask (agent stops at the re-probe), or the lettered
|
||||
// gstack drive / manual / defer question (any letter — agents keep the
|
||||
// contract's B/C/D lettering with the Aside option dropped, observed live).
|
||||
const askedToOpen = /open the Aside app/i.test(text);
|
||||
const letteredQuestion = /\b[A-D]\)/.test(text);
|
||||
expect({ askedToOpen, letteredQuestion, ok: askedToOpen || letteredQuestion })
|
||||
.toMatchObject({ ok: true });
|
||||
} finally { cleanup(); }
|
||||
}, 6 * 60_000);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user