stably-sdk-rules — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited stably-sdk-rules (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
agent.act() for canvas, coordinate-based drag/click, or unstable multi-step flows.expect(...).aiAssert() for dynamic visual assertions; keep prompts specific.page.extract() / locator.extract() when you need visual-to-data extraction.page.getLocatorsByAI() when semantic selectors are hard with standard locators.Inbox for OTP/magic-link/verification email flows.agent.act() tasks small; do loops/calculations/conditionals in code.fullPage: true only if content outside viewport matters..describe("...") to locators for trace readability.Inbox.build({ suffix }) per test and clean up.npm install -D @playwright/test @stablyai/playwright-test @stablyai/email
export STABLY_API_KEY=YOUR_KEY
export STABLY_PROJECT_ID=YOUR_PROJECT_IDimport { test, expect } from "@stablyai/playwright-test";
import { Inbox } from "@stablyai/email";Optional: set API key programmatically.
import { setApiKey } from "@stablyai/playwright-test";
setApiKey("YOUR_KEY");.describe("...").aiAssert for dynamic/visual-heavy checks.agent.act for brittle or semantic tasks (especially canvas/coordinates).aiAssertawait expect(page).aiAssert("Shows revenue trend chart and spotlight card");
await expect(page.locator(".header").describe("Header")).aiAssert("Has nav, avatar, and bell icon");Use fullPage: true only when assertion needs off-screen content.
extractconst orderId = await page.extract("Extract the order ID from the first row");With schema:
import { z } from "zod";
const Schema = z.object({ revenue: z.string(), users: z.number() });
const metrics = await page.extract("Get revenue and active users", { schema: Schema });getLocatorsByAIRequires Playwright >= 1.54.1.
const { locator, count } = await page.getLocatorsByAI("the login button");
expect(count).toBe(1);
await locator.describe("Login button located by AI").click();agent.actawait agent.act("Find the first pending order and mark it as shipped", { page });Good pattern: compute values in code, then pass concrete values into the prompt.
Inbox (Email Isolation)Install: npm install -D @stablyai/email. Requires STABLY_API_KEY and STABLY_PROJECT_ID env vars (or pass to Inbox.build()).
const inbox = await Inbox.build({ suffix: `test-${Date.now()}` });
// inbox.address → "[email protected]"
await page.getByLabel("Email").describe("Email input").fill(inbox.address);
const email = await inbox.waitForEmail({ subject: "verification", timeoutMs: 60_000 });
const { data: otp } = await inbox.extractFromEmail({
id: email.id,
prompt: "Extract the 6-digit OTP code",
});
await inbox.deleteAllEmails();#### Inbox.build Options
| Option | Type | Description |
|---|---|---|
suffix | string | Suffix for test isolation (e.g., "test-123" → "[email protected]") |
apiKey | string | Defaults to STABLY_API_KEY env var |
projectId | string | Defaults to STABLY_PROJECT_ID env var |
Always use a unique suffix per test for parallel isolation. The inbox automatically filters out emails received before it was created.
#### waitForEmail
const email = await inbox.waitForEmail({
from: "[email protected]", // filter by sender
subject: "verification", // contains match by default
subjectMatch: "exact", // or "contains" (default)
timeoutMs: 60_000, // default: 120000 (2 min)
pollIntervalMs: 5000, // default: 3000 (3 sec)
});Throws EmailTimeoutError if no match arrives within the timeout.
#### extractFromEmail
Returns { data, reason }. Throws EmailExtractionError on failure.
// String extraction
const { data: otp } = await inbox.extractFromEmail({
id: email.id,
prompt: "Extract the 6-digit OTP code",
});
// Structured extraction with Zod schema
import { z } from "zod";
const { data } = await inbox.extractFromEmail({
id: email.id,
prompt: "Extract the verification URL and expiration time",
schema: z.object({ url: z.string().url(), expiresIn: z.string() }),
});#### Inbox Properties
| Property | Type | Description | |
|---|---|---|---|
address | string | Full email address (with suffix if provided) | |
suffix | string \ | undefined | The suffix passed to Inbox.build() |
createdAt | Date | Inbox creation time; emails before this are auto-filtered |
#### listEmails
const { emails, nextCursor } = await inbox.listEmails(options?);| Option | Type | Default | Description | |
|---|---|---|---|---|
from | string | — | Filter by sender address | |
subject | string | — | Filter by subject | |
subjectMatch | 'contains' \ | 'exact' | 'contains' | Subject matching mode |
limit | number | 20 | Max results (max: 100) | |
cursor | string | — | Pagination cursor from previous nextCursor | |
since | Date | — | Override the default creation-time filter | |
includeOlder | boolean | false | Include emails received before inbox creation |
#### Other Methods
const email = await inbox.getEmail(id); // get by ID
await inbox.deleteEmail(email.id); // delete single
await inbox.deleteAllEmails(); // delete all (this inbox only)#### Email Object Properties
| Property | Type | Description |
|---|---|---|
id | string | Unique identifier |
mailbox | string | Container (e.g., "INBOX") |
from | { address: string, name?: string } | Sender |
to | { address: string, name?: string }[] | Recipients |
subject | string | Subject line |
receivedAt | Date | Arrival timestamp |
text | string? | Plain text body |
html | string[]? | HTML body parts |
#### Playwright Fixture Pattern
import { test as base } from "@stablyai/playwright-test";
import { Inbox } from "@stablyai/email";
const test = base.extend<{ inbox: Inbox }>({
inbox: async ({}, use, testInfo) => {
const inbox = await Inbox.build({ suffix: `test-${testInfo.testId}` });
await use(inbox);
await inbox.deleteAllEmails();
},
});
test("signup flow", async ({ page, inbox }) => {
await page.fill("#email", inbox.address);
await page.click("#signup");
const email = await inbox.waitForEmail({ subject: "Welcome" });
// ...
});Your email address is visible in the Stably dashboard:
inbox.address after calling Inbox.build() returns your full addressThe pattern is {org-name}@mail.stably.ai. If the user needs to allowlist, they should add mail.stably.ai to their email provider's allowlist.
Direct users to the dashboard Settings > Email Inbox to find their specific address.
Use the helper instead of custom popup scripting:
import { authWithGoogle } from "@stablyai/playwright-test/auth";
await authWithGoogle({
context,
email: process.env.GOOGLE_AUTH_EMAIL!,
password: process.env.GOOGLE_AUTH_PASSWORD!,
otpSecret: process.env.GOOGLE_AUTH_OTP_SECRET!,
});Required env vars:
GOOGLE_AUTH_EMAILGOOGLE_AUTH_PASSWORDGOOGLE_AUTH_OTP_SECRETUse a dedicated test Google account only.
aiAssert is slow/flaky: scope to a locator, tighten prompt, avoid unnecessary fullPage: true.agent.act fails: split into smaller tasks, pass explicit constraints, raise maxCycles only when needed.skills/stably-sdk-setup/SKILL.mdskills/stably-sdk-rules/README.md~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.