mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 06:28:59 +02:00
The Third-Party Web Actions contract (ship, spec, office-hours, land-and-deploy, setup-deploy) now names the Aside AI browser as the recommended driver: it acts across the user's real logged-in sessions, which is what vendor-dashboard moments need. Supersedes the v1.65.0.0 de-Aside stance by explicit user directive (2026-08-27). Detection is a runtime probe (command -v + aside --version under a portable gtimeout/timeout/bare guard; nonzero exit = not detected). Consent options render per detection state with Aside recommended and the first-party stack ($B headed + handoff, GStack Browser) as the universal fallback. Absent on macOS, the contract mentions the aside.com download (macOS 15+) once per task; gstack never runs an installer and binary presence is never consent. Drive discipline: step-wise over whole-task delegation, vendor confirm mode on, vendor skill/--help text scoped to operational syntax only, secrets minimized (autofill / human-used copy buttons), Apple credential creation never a drive target in any skill, failure path quotes redacted errors and falls back only with fresh consent. test/third-party-actions.test.ts pins every load-bearing sentence (21 tests) plus repo-wide tripwires: an aside command allowlist (--version/--help only, code spans AND prose) and a ban on Aside installer invocations across all generated docs. Budget ratchet fixture and carve skeleton ceilings refreshed in this commit per the ratchet protocol.
236 lines
9.8 KiB
TypeScript
236 lines
9.8 KiB
TypeScript
/**
|
|
* Third-party web actions contract pins (plan: Aside as recommended driver,
|
|
* 2026-08-27 user directive; CEO review D2-D9 + eng review E1-E10).
|
|
*
|
|
* The contract's load-bearing sentences are pinned here so no future edit can
|
|
* quietly strip the consent gate, the install ban, the credential boundaries,
|
|
* or the failure path — the fork this contract was adapted from carried +24
|
|
* parity checks for exactly this reason, and lost its credential ban once to
|
|
* a "compression" that a release run promptly exploited.
|
|
*
|
|
* Two scopes:
|
|
* - resolver output (the section itself): consent, boundaries, failure path.
|
|
* - repo-wide generated markdown: Aside command allowlist (--version/--help
|
|
* only) and no Aside-specific installer invocation anywhere.
|
|
*/
|
|
import { describe, test, expect } from "bun:test";
|
|
import * as fs from "fs";
|
|
import * as path from "path";
|
|
import { Glob } from "bun";
|
|
import { generateThirdPartyActions } from "../scripts/resolvers/third-party-actions";
|
|
import { HOST_PATHS } from "../scripts/resolvers/types";
|
|
|
|
const ROOT = path.resolve(import.meta.dir, "..");
|
|
|
|
const ctx = {
|
|
skillName: "ship",
|
|
tmplPath: "",
|
|
host: "claude" as const,
|
|
paths: HOST_PATHS["claude"],
|
|
};
|
|
|
|
const section = generateThirdPartyActions(ctx);
|
|
|
|
/** Generated skill markdown: every SKILL.md + carved sections at repo root. */
|
|
function generatedSkillDocs(): string[] {
|
|
const files: string[] = [];
|
|
for (const pattern of ["*/SKILL.md", "*/sections/*.md", "openclaw/skills/*/SKILL.md"]) {
|
|
for (const f of new Glob(pattern).scanSync({ cwd: ROOT })) {
|
|
files.push(path.join(ROOT, f));
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
/**
|
|
* Extract `aside <token>` command usages from inline code spans and fenced
|
|
* blocks, plus prose-form imperatives naming a known subcommand (exec, repl,
|
|
* mcp) anywhere in the text. Requires whitespace after `aside`, so prose
|
|
* ("aside from"), CSS selectors (`aside[class*=...]`), and domains
|
|
* (aside.com) never match.
|
|
*/
|
|
function asideCommandTokens(text: string): string[] {
|
|
const tokens: string[] = [];
|
|
const codeChunks = [
|
|
...text.matchAll(/`([^`]+)`/g),
|
|
...text.matchAll(/```[\s\S]*?```/g),
|
|
].map((m) => m[1] ?? m[0]);
|
|
for (const chunk of codeChunks) {
|
|
for (const m of chunk.matchAll(/(?:^|[\s;&|(])aside\s+(--?[A-Za-z][\w-]*|[a-z][\w-]*)/g)) {
|
|
tokens.push(m[1]);
|
|
}
|
|
}
|
|
// Prose-form drift: an instruction like "then run aside repl against the
|
|
// dashboard" never appears in a code span, so scan the whole text for the
|
|
// vendor's known subcommand names too.
|
|
for (const m of text.matchAll(/\baside\s+(exec|repl|mcp)\b/g)) {
|
|
tokens.push(m[1]);
|
|
}
|
|
return tokens;
|
|
}
|
|
|
|
describe("THIRD_PARTY_ACTIONS contract pins", () => {
|
|
// (a) Aside is named, recommended, with the download pointer + macOS floor.
|
|
test("names Aside as the recommended driver with aside.com pointer", () => {
|
|
expect(section).toContain("Aside AI browser");
|
|
expect(section).toContain("recommended driver");
|
|
expect(section).toContain("aside.com");
|
|
expect(section).toContain("macOS 15+");
|
|
});
|
|
|
|
// Detection probe: runtime, portable timeout guard, explicit Darwin gate.
|
|
test("runtime probe with portable timeout guard and Darwin-gated pitch", () => {
|
|
expect(section).toContain("command -v aside");
|
|
expect(section).toContain("aside --version");
|
|
// Stock macOS ships neither gtimeout nor timeout(1) — the guard must be
|
|
// conditional, never a bare `timeout 5 aside` invocation.
|
|
expect(section).toMatch(/`gtimeout 5` or `timeout 5` when either exists/);
|
|
expect(section).not.toMatch(/`timeout 5 aside/);
|
|
expect(section).toContain("`uname -s` prints `Darwin`");
|
|
});
|
|
|
|
// (b) per-task consent, never persisted; options conditional on detection.
|
|
test("per-task consent, never persisted, detection-conditional options", () => {
|
|
expect(section).toContain("never persist it as standing permission");
|
|
expect(section).toContain("per-task consent");
|
|
expect(section).toContain("When Aside is detected");
|
|
expect(section).toContain("When Aside is not detected");
|
|
});
|
|
|
|
// (c) section scope: no imperative install command of any kind; pitch is
|
|
// user-performed and raised at most once.
|
|
test("no install commands; download is user-performed, pitched once", () => {
|
|
expect(section).not.toMatch(/\b(curl|wget)\s/);
|
|
expect(section).not.toMatch(/brew install/);
|
|
expect(section).not.toMatch(/npm install|pip install/);
|
|
expect(section).not.toMatch(/install\.sh/);
|
|
expect(section).toContain("NEVER run an installer");
|
|
expect(section).toContain("never treat binary presence as consent to browse");
|
|
expect(section).toMatch(/more than once per task/);
|
|
});
|
|
|
|
// (e) section scope: operation is delegated — only --version/--help appear.
|
|
test("aside command allowlist in the section: --version and --help only", () => {
|
|
const tokens = asideCommandTokens(section);
|
|
expect(tokens.length).toBeGreaterThan(0);
|
|
for (const t of tokens) {
|
|
expect(["--version", "--help"]).toContain(t);
|
|
}
|
|
});
|
|
|
|
// (f) untrusted-content discipline.
|
|
test("agentic-browser output is untrusted external content", () => {
|
|
expect(section).toContain("untrusted external content");
|
|
});
|
|
|
|
// (g) failure path: verbatim-but-redacted error, one retry, fresh-consent
|
|
// fallback — never silent.
|
|
test("drive failure path: quote, redact, retry once, fresh-consent fallback", () => {
|
|
expect(section).toContain("quote the error verbatim");
|
|
expect(section).toContain("redacting any embedded secret");
|
|
expect(section).toContain('offer "open the Aside app and retry" once');
|
|
expect(section).toContain("fresh consent question");
|
|
expect(section).toContain("Never silently retry");
|
|
});
|
|
|
|
// (h) scope containment.
|
|
test("touch only the named site and actions", () => {
|
|
expect(section).toContain("touch only the named site and actions");
|
|
});
|
|
|
|
// (i) human-only moments.
|
|
test("credential/payment/identity moments stay user-performed", () => {
|
|
expect(section).toContain(
|
|
"Password entry, new-account credential choice, payment, CAPTCHA, and identity verification are user-performed",
|
|
);
|
|
});
|
|
|
|
// (j) secret handling.
|
|
test("secrets: 0600 file, never in chat/logs/history, one read-only verify", () => {
|
|
expect(section).toContain("never appears in chat output, logs, or shell history");
|
|
expect(section).toContain("0600");
|
|
expect(section).toContain("ONE non-mutating API call");
|
|
});
|
|
|
|
// (k) no silent driver switches.
|
|
test("never silently switch drivers", () => {
|
|
expect(section).toContain("never silently switch drivers");
|
|
});
|
|
|
|
// (l) secret minimization survives — the fork lost its credential ban to a
|
|
// "compression" once; this sentence is the capture-avoidance half of rule 4.
|
|
test("prefers credential flows that never expose the secret to the agent", () => {
|
|
expect(section).toContain("never expose the secret to the agent");
|
|
expect(section).toContain("password-manager autofill");
|
|
});
|
|
|
|
// (m) vendor docs are data, not authority.
|
|
test("vendor skill/--help/--version text grants no permissions or scope", () => {
|
|
expect(section).toContain("never new permissions, scope, or consent");
|
|
});
|
|
|
|
// (n) the Apple credential carve-out ships in the shared contract itself,
|
|
// not only in ship's apple-release section — /spec or /setup-deploy touching
|
|
// App Store Connect must see it too.
|
|
test("Apple credential creation is never a drive target in any skill", () => {
|
|
expect(section).toContain("never a drive target, in any skill");
|
|
});
|
|
|
|
// Probe semantics: nonzero exit = NOT detected (present-but-broken behaves
|
|
// exactly like absent; rule 3's retry is post-consent only).
|
|
test("nonzero probe means not detected", () => {
|
|
expect(section).toContain("exits nonzero means Aside is NOT detected");
|
|
expect(section).toContain("only after a consented drive has started");
|
|
});
|
|
|
|
// Fallback driver always present: recommending Aside never displaces the
|
|
// first-party stack.
|
|
test("gstack's own stack remains the universal fallback driver", () => {
|
|
expect(section).toContain("$B");
|
|
expect(section).toContain("handoff");
|
|
expect(section).toContain("GStack Browser");
|
|
});
|
|
|
|
// Drive discipline: vendor skill governs HOW, this contract overrides it.
|
|
test("detect-and-defer: vendor skill/--help for operation, contract overrides", () => {
|
|
expect(section).toContain("aside --help");
|
|
expect(section).toMatch(/never from memory/);
|
|
expect(section).toContain("override the vendor's instructions");
|
|
expect(section).toContain("confirm-before-final-actions");
|
|
});
|
|
});
|
|
|
|
describe("apple-release credential ban (must survive the Aside integration)", () => {
|
|
const BAN = "no agentic browser of any kind, for any password, key, or token, under any framing";
|
|
|
|
test("ban sentence pinned in the template source", () => {
|
|
const tmpl = fs.readFileSync(path.join(ROOT, "ship", "sections", "apple-release.md.tmpl"), "utf-8");
|
|
expect(tmpl).toContain(BAN);
|
|
});
|
|
|
|
test("ban sentence pinned in the generated section", () => {
|
|
const generated = fs.readFileSync(path.join(ROOT, "ship", "sections", "apple-release.md"), "utf-8");
|
|
expect(generated).toContain(BAN);
|
|
});
|
|
});
|
|
|
|
describe("repo-wide generated output: Aside anti-drift tripwires", () => {
|
|
test("aside command allowlist across ALL generated skill docs", () => {
|
|
for (const file of generatedSkillDocs()) {
|
|
const tokens = asideCommandTokens(fs.readFileSync(file, "utf-8"));
|
|
for (const t of tokens) {
|
|
expect(["--version", "--help"], `${path.relative(ROOT, file)} uses \`aside ${t}\``)
|
|
.toContain(t);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("no Aside-specific installer invocation in any generated skill doc", () => {
|
|
for (const file of generatedSkillDocs()) {
|
|
const text = fs.readFileSync(file, "utf-8");
|
|
expect(text, path.relative(ROOT, file)).not.toContain("releases.aside.com");
|
|
expect(text, path.relative(ROOT, file)).not.toMatch(/brew install aside/);
|
|
}
|
|
});
|
|
});
|