pi-ui-extend 1.0.21 → 1.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/external/pi-tools-suite/README.md +11 -8
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +5 -5
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +2 -2
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/SKILL.md +4 -2
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +9 -6
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/scripts/browser-qa-runner.mjs +15 -35
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +7 -0
- package/package.json +1 -1
|
@@ -247,10 +247,10 @@ For an oh-my-openagent-style workflow, run `/ultrawork` or `/ulw` to ask the par
|
|
|
247
247
|
|
|
248
248
|
### Private browser QA and project auth
|
|
249
249
|
|
|
250
|
-
The built-in `browser-qa` role runs on `
|
|
251
|
-
`
|
|
252
|
-
private skill under `src/async-subagents/private-skills/`,
|
|
253
|
-
discovery. The role's first-class `isolatedSkills` setting launches the child with
|
|
250
|
+
The built-in `browser-qa` role runs on `openai-codex/gpt-5.4-mini`, with
|
|
251
|
+
`antigravity/gemini-3-flash-preview` and then `zai/glm-5.3` as fallbacks. Its browser
|
|
252
|
+
workflow is an explicit private skill under `src/async-subagents/private-skills/`,
|
|
253
|
+
outside normal Pi skill discovery. The role's first-class `isolatedSkills` setting launches the child with
|
|
254
254
|
`--no-skills` plus one self-contained private workflow. It bundles the relevant
|
|
255
255
|
scenario-design, locator, waiting, assertion, evidence, and cleanup guidance next
|
|
256
256
|
to its trusted runner, so browser QA does not depend on a separately installed
|
|
@@ -288,10 +288,13 @@ blocked. Example:
|
|
|
288
288
|
}
|
|
289
289
|
```
|
|
290
290
|
|
|
291
|
-
Do not place credential values in prompts, QA flows, shell arguments, reports
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
291
|
+
Do not place credential values in prompts, QA flows, shell arguments, or reports.
|
|
292
|
+
The helper reads JSONC internally and emits only redacted statuses. For form auth,
|
|
293
|
+
video recording begins on the login page and captures the field-filling and submit
|
|
294
|
+
sequence; password inputs remain browser-masked, but the private video may show
|
|
295
|
+
other visible login identifiers and must be treated as sensitive evidence. Tracing
|
|
296
|
+
starts only after login succeeds and is sanitized before retention. The launcher
|
|
297
|
+
provides each browser QA process with its own
|
|
295
298
|
`.pi/subagents/<run>/<agent-id>/browser-qa/` workspace. Declarative flows,
|
|
296
299
|
screenshots, video, sanitized traces, and result manifests stay there, so normal
|
|
297
300
|
session shutdown or `subagents cleanup` removes them with the run directory.
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
"research": { "model": "zai/glm-5-turbo", "thinking": "low" },
|
|
117
117
|
"docs": { "model": "zai/glm-4.5-air", "thinking": "low" },
|
|
118
118
|
"frontend": { "model": "antigravity/gemini-3-flash-preview", "fallbackModels": ["zai/glm-5.3"], "thinking": "medium" },
|
|
119
|
-
"browser-qa": { "model": "
|
|
119
|
+
"browser-qa": { "model": "openai-codex/gpt-5.4-mini", "fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"], "thinking": "medium" },
|
|
120
120
|
"tests": { "model": "zai/glm-5-turbo", "thinking": "medium" },
|
|
121
121
|
"review": { "model": "zai/glm-5.3", "thinking": "high" },
|
|
122
122
|
"implement": { "model": "zai/glm-5.3", "thinking": "high" },
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"research": { "model": "openai-codex/gpt-5.6-terra", "fallbackModels": ["zai/glm-5-turbo"], "thinking": "low" },
|
|
133
133
|
"docs": { "model": "openai-codex/gpt-5.6-luna", "fallbackModels": ["zai/glm-4.5-air"], "thinking": "low" },
|
|
134
134
|
"frontend": { "model": "openai-codex/gpt-5.6-terra", "fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"], "thinking": "medium" },
|
|
135
|
-
"browser-qa": { "model": "
|
|
135
|
+
"browser-qa": { "model": "openai-codex/gpt-5.4-mini", "fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"], "thinking": "medium" },
|
|
136
136
|
"tests": { "model": "openai-codex/gpt-5.6-terra", "fallbackModels": ["zai/glm-5-turbo"], "thinking": "medium" },
|
|
137
137
|
"review": { "model": "openai-codex/gpt-5.6-sol", "fallbackModels": ["zai/glm-5.3"], "thinking": "high" },
|
|
138
138
|
"implement": { "model": "openai-codex/gpt-5.6-sol", "fallbackModels": ["zai/glm-5.3"], "thinking": "high" },
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
"research": { "model": "antigravity/gemini-3.1-pro-preview", "fallbackModels": ["openai-codex/gpt-5.4-mini", "zai/glm-5-turbo"], "thinking": "medium" },
|
|
149
149
|
"docs": { "model": "antigravity/gemini-2.5-flash", "fallbackModels": ["openai-codex/gpt-5.3-codex-spark", "zai/glm-4.5-air"], "thinking": "medium" },
|
|
150
150
|
"frontend": { "model": "antigravity/gemini-3.1-pro-preview-customtools", "fallbackModels": ["openai-codex/gpt-5.4-mini", "zai/glm-5.3"], "thinking": "low" },
|
|
151
|
-
"browser-qa": { "model": "
|
|
151
|
+
"browser-qa": { "model": "openai-codex/gpt-5.4-mini", "fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"], "thinking": "medium" },
|
|
152
152
|
"tests": { "model": "antigravity/antigravity-claude-sonnet-4-6", "fallbackModels": ["openai-codex/gpt-5.4-mini", "zai/glm-5-turbo"], "thinking": "high" },
|
|
153
153
|
"review": { "model": "antigravity/antigravity-claude-sonnet-4-6", "fallbackModels": ["openai-codex/gpt-5.6-sol", "zai/glm-5.3"], "thinking": "high" },
|
|
154
154
|
"implement": { "model": "openai-codex/gpt-5.6-sol", "fallbackModels": ["zai/glm-5.3"], "thinking": "high" },
|
|
@@ -199,8 +199,8 @@
|
|
|
199
199
|
|
|
200
200
|
"browser-qa": {
|
|
201
201
|
"description": "Use for browser-based visual QA: reproduce UI bugs and verify fixes with deterministic assertions, screenshots, video, and traces.",
|
|
202
|
-
"model": "
|
|
203
|
-
"fallbackModels": ["
|
|
202
|
+
"model": "openai-codex/gpt-5.4-mini",
|
|
203
|
+
"fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"],
|
|
204
204
|
"thinking": "medium",
|
|
205
205
|
"tools": ["read", "grep", "bash"]
|
|
206
206
|
},
|
|
@@ -207,8 +207,8 @@ const BUILTIN_CONFIG: SubagentConfig = {
|
|
|
207
207
|
},
|
|
208
208
|
"browser-qa": {
|
|
209
209
|
description: "Use for browser-based visual QA: reproduce UI bugs and verify fixes with deterministic assertions, screenshots, video, and traces.",
|
|
210
|
-
model: "
|
|
211
|
-
fallbackModels: ["
|
|
210
|
+
model: "openai-codex/gpt-5.4-mini",
|
|
211
|
+
fallbackModels: ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"],
|
|
212
212
|
thinking: "medium",
|
|
213
213
|
tools: ["read", "grep", "bash"],
|
|
214
214
|
isolatedSkills: [getBrowserQaSkillPath()],
|
|
@@ -101,8 +101,10 @@ isolated browser context and exclusive evidence directory; the runner closes
|
|
|
101
101
|
all owned browser resources on success and failure. Flows, screenshots, video,
|
|
102
102
|
sanitized traces, and runner result manifests remain under
|
|
103
103
|
`$PI_SUBAGENT_AGENT_DIR/browser-qa/` so normal sub-agent shutdown or cleanup
|
|
104
|
-
deletes them with the run directory.
|
|
105
|
-
|
|
104
|
+
deletes them with the run directory. For form auth, recording starts on the login
|
|
105
|
+
page and includes field filling and submission; password inputs remain masked,
|
|
106
|
+
but the private video may show visible login identifiers. Tracing starts only
|
|
107
|
+
after login succeeds so credentials are not captured in the trace.
|
|
106
108
|
|
|
107
109
|
## Credentials and blocked runs
|
|
108
110
|
|
|
@@ -55,13 +55,16 @@ stale credentials into an explicit update request instead of misreporting a
|
|
|
55
55
|
product regression.
|
|
56
56
|
|
|
57
57
|
Do not encode credentials, tokens, storage values, or login form secrets in the
|
|
58
|
-
flow. The trusted runner applies the selected profile internally
|
|
59
|
-
|
|
58
|
+
flow. The trusted runner applies the selected profile internally. For form auth,
|
|
59
|
+
video starts on the login page and includes field filling and submission; password
|
|
60
|
+
inputs remain masked, but visible identifiers can appear, so treat the video as
|
|
61
|
+
sensitive private evidence. Tracing starts only after login succeeds and is
|
|
62
|
+
sanitized before retention.
|
|
60
63
|
|
|
61
64
|
## Evidence strategy
|
|
62
65
|
|
|
63
|
-
The runner always attempts a final or failure screenshot, records video
|
|
64
|
-
creates a sanitized trace
|
|
66
|
+
The runner always attempts a final or failure screenshot, records video from the
|
|
67
|
+
first page, and creates a sanitized post-auth trace. Add named `screenshot`
|
|
65
68
|
steps only at states that materially help explain the result—for example before
|
|
66
69
|
and after a destructive interaction, or when a transient success message is
|
|
67
70
|
the oracle.
|
|
@@ -102,5 +105,5 @@ result manifest inside `$PI_SUBAGENT_AGENT_DIR/browser-qa/`. The launcher owns
|
|
|
102
105
|
that path and the runner validates it before opening a browser. Do not override
|
|
103
106
|
the environment path or copy evidence into shared `.pi/qa-runs`/`.pi/qa-flows`
|
|
104
107
|
directories: the agent-local workspace is intentionally removed by the normal
|
|
105
|
-
sub-agent shutdown and cleanup lifecycle. Authentication config
|
|
106
|
-
|
|
108
|
+
sub-agent shutdown and cleanup lifecycle. Authentication config remains a
|
|
109
|
+
separate persistent input under project `.pi/`.
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
3
|
import fs from "node:fs";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import { createRequire } from "node:module";
|
|
@@ -9,10 +8,10 @@ import { parse as parseJsonc, printParseErrorCode } from "jsonc-parser";
|
|
|
9
8
|
import { strFromU8, strToU8, unzipSync, zipSync } from "../vendor/fflate.mjs";
|
|
10
9
|
|
|
11
10
|
const CONFIG_RELATIVE = ".pi/qa_auth.jsonc";
|
|
12
|
-
const STATE_RELATIVE = path.join(".pi", "qa-auth-state");
|
|
13
11
|
const SUBAGENT_AGENT_DIR_ENV = "PI_SUBAGENT_AGENT_DIR";
|
|
14
12
|
const QA_WORKSPACE_RELATIVE = "browser-qa";
|
|
15
13
|
const EVIDENCE_RELATIVE = "evidence";
|
|
14
|
+
const FORM_VIDEO_STEP_DELAY_MS = 250;
|
|
16
15
|
const EXIT_AUTH_UPDATE_REQUIRED = 42;
|
|
17
16
|
const EXIT_PROFILE_REQUIRED = 43;
|
|
18
17
|
const PROFILE_ID = /^[A-Za-z0-9._-]+$/;
|
|
@@ -112,7 +111,7 @@ async function runQa({ cwd, agentDir, args, profileId, profile }) {
|
|
|
112
111
|
const runtimeSecrets = collectSecrets(profile.auth);
|
|
113
112
|
const tracePath = path.join(evidenceDir, "trace.zip");
|
|
114
113
|
try {
|
|
115
|
-
const contextOptions = await contextOptionsForAuth({ cwd, profileId, profile, allowedOrigins
|
|
114
|
+
const contextOptions = await contextOptionsForAuth({ cwd, profileId, profile, allowedOrigins });
|
|
116
115
|
context = await browser.newContext({
|
|
117
116
|
...contextOptions,
|
|
118
117
|
baseURL,
|
|
@@ -122,10 +121,11 @@ async function runQa({ cwd, agentDir, args, profileId, profile }) {
|
|
|
122
121
|
});
|
|
123
122
|
await installOriginGuard(context, allowedOrigins, profile.auth);
|
|
124
123
|
await applyContextAuth(context, profile.auth, allowedOrigins, baseURL, profileId);
|
|
125
|
-
runtimeSecrets.push(...collectStorageStateSecrets(await context.storageState()));
|
|
126
|
-
await context.tracing.start({ screenshots: true, snapshots: true, sources: false });
|
|
127
124
|
page = await context.newPage();
|
|
128
125
|
video = page.video();
|
|
126
|
+
await applyFormAuth(page, profile.auth, allowedOrigins, profileId);
|
|
127
|
+
runtimeSecrets.push(...collectStorageStateSecrets(await context.storageState()));
|
|
128
|
+
await context.tracing.start({ screenshots: true, snapshots: true, sources: false });
|
|
129
129
|
await executeFlow({ page, context, baseURL, evidenceDir, flow, allowedOrigins, profileId, secrets: runtimeSecrets });
|
|
130
130
|
await assertPageDoesNotExposeSecrets(page, runtimeSecrets, profileId);
|
|
131
131
|
await page.screenshot({ path: path.join(evidenceDir, "final.png"), fullPage: true });
|
|
@@ -465,7 +465,7 @@ function isStringArray(value) {
|
|
|
465
465
|
return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string");
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
-
async function contextOptionsForAuth({ cwd, profileId, profile, allowedOrigins
|
|
468
|
+
async function contextOptionsForAuth({ cwd, profileId, profile, allowedOrigins }) {
|
|
469
469
|
const auth = profile.auth;
|
|
470
470
|
if (auth.type === "storageState") {
|
|
471
471
|
if (typeof auth.path !== "string") throw authError(profileId, "storageState path is missing");
|
|
@@ -473,16 +473,7 @@ async function contextOptionsForAuth({ cwd, profileId, profile, allowedOrigins,
|
|
|
473
473
|
return { storageState: filteredStorageState(statePath, allowedOrigins, profileId) };
|
|
474
474
|
}
|
|
475
475
|
if (auth.type === "form") {
|
|
476
|
-
|
|
477
|
-
const statePath = path.join(stateDirectory, `${profileId}-${authCacheKey(auth, allowedOrigins)}.json`);
|
|
478
|
-
if (!fs.existsSync(statePath)) {
|
|
479
|
-
ensurePrivateDirectory(cwd, stateDirectory);
|
|
480
|
-
for (const name of fs.readdirSync(stateDirectory)) {
|
|
481
|
-
if (name.startsWith(`${profileId}-`) && name.endsWith(".json")) fs.rmSync(path.join(stateDirectory, name), { force: true });
|
|
482
|
-
}
|
|
483
|
-
await createFormState({ statePath, auth, allowedOrigins, browser, profileId });
|
|
484
|
-
}
|
|
485
|
-
return { storageState: filteredStorageState(statePath, allowedOrigins, profileId) };
|
|
476
|
+
return {};
|
|
486
477
|
}
|
|
487
478
|
if (!["cookie", "localStorage", "sessionStorage", "bearer"].includes(auth.type)) {
|
|
488
479
|
throw authError(profileId, `unsupported auth type: ${auth.type}`);
|
|
@@ -490,39 +481,36 @@ async function contextOptionsForAuth({ cwd, profileId, profile, allowedOrigins,
|
|
|
490
481
|
return {};
|
|
491
482
|
}
|
|
492
483
|
|
|
493
|
-
async function
|
|
484
|
+
async function applyFormAuth(page, auth, allowedOrigins, profileId) {
|
|
485
|
+
if (auth.type !== "form") return;
|
|
494
486
|
if (typeof auth.loginUrl !== "string" || !isAllowedUrl(auth.loginUrl, allowedOrigins)) {
|
|
495
487
|
throw authError(profileId, "form loginUrl is missing or outside allowedOrigins");
|
|
496
488
|
}
|
|
497
489
|
if (!Array.isArray(auth.fields) || auth.fields.length === 0 || typeof auth.submitSelector !== "string") {
|
|
498
490
|
throw authError(profileId, "form fields or submitSelector are missing");
|
|
499
491
|
}
|
|
500
|
-
const context = await browser.newContext();
|
|
501
492
|
try {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
493
|
+
const timeout = Math.min(finitePositive(auth.timeoutMs) ?? 15_000, 60_000);
|
|
494
|
+
page.setDefaultTimeout(timeout);
|
|
495
|
+
page.setDefaultNavigationTimeout(timeout);
|
|
496
|
+
await page.goto(auth.loginUrl, { timeout });
|
|
497
|
+
await page.waitForTimeout(FORM_VIDEO_STEP_DELAY_MS);
|
|
505
498
|
for (const field of auth.fields) {
|
|
506
499
|
if (!isObject(field) || typeof field.selector !== "string" || typeof field.value !== "string") {
|
|
507
500
|
throw authError(profileId, "form fields must contain selector/value strings");
|
|
508
501
|
}
|
|
509
502
|
await page.locator(field.selector).fill(field.value);
|
|
503
|
+
await page.waitForTimeout(FORM_VIDEO_STEP_DELAY_MS);
|
|
510
504
|
}
|
|
511
505
|
await page.locator(auth.submitSelector).click();
|
|
512
|
-
const timeout = finitePositive(auth.timeoutMs) ?? 15_000;
|
|
513
506
|
if (isObject(auth.success) && typeof auth.success.url === "string") await page.waitForURL(auth.success.url, { timeout });
|
|
514
507
|
if (isObject(auth.success) && typeof auth.success.selector === "string") await page.locator(auth.success.selector).waitFor({ timeout });
|
|
515
508
|
if (!isObject(auth.success) || (typeof auth.success.url !== "string" && typeof auth.success.selector !== "string")) {
|
|
516
509
|
throw authError(profileId, "form success.url or success.selector is required");
|
|
517
510
|
}
|
|
518
|
-
fs.mkdirSync(path.dirname(statePath), { recursive: true, mode: 0o700 });
|
|
519
|
-
await context.storageState({ path: statePath });
|
|
520
|
-
fs.chmodSync(statePath, 0o600);
|
|
521
511
|
} catch (error) {
|
|
522
512
|
if (error instanceof QaStatusError) throw error;
|
|
523
513
|
throw authError(profileId, "form login was rejected or did not reach the configured success condition");
|
|
524
|
-
} finally {
|
|
525
|
-
await context.close().catch(() => {});
|
|
526
514
|
}
|
|
527
515
|
}
|
|
528
516
|
|
|
@@ -550,10 +538,6 @@ async function applyContextAuth(context, auth, allowedOrigins, baseURL, profileI
|
|
|
550
538
|
}
|
|
551
539
|
}
|
|
552
540
|
|
|
553
|
-
function authCacheKey(auth, allowedOrigins) {
|
|
554
|
-
return createHash("sha256").update(JSON.stringify({ auth, allowedOrigins })).digest("hex").slice(0, 16);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
541
|
async function installOriginGuard(context, allowedOrigins, auth) {
|
|
558
542
|
await context.route("**/*", async (route) => {
|
|
559
543
|
const request = route.request();
|
|
@@ -890,10 +874,6 @@ function createExclusivePrivateDirectory(cwd, target) {
|
|
|
890
874
|
createPrivateDirectory(cwd, target, true);
|
|
891
875
|
}
|
|
892
876
|
|
|
893
|
-
function ensurePrivateDirectory(cwd, target) {
|
|
894
|
-
createPrivateDirectory(cwd, target, false);
|
|
895
|
-
}
|
|
896
|
-
|
|
897
877
|
function createPrivateDirectory(cwd, target, exclusive) {
|
|
898
878
|
const root = fs.realpathSync(cwd);
|
|
899
879
|
const resolved = path.resolve(target);
|
|
@@ -300,6 +300,13 @@ export const DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC = String.raw`{
|
|
|
300
300
|
"When no mockup exists, choose a clear aesthetic direction and explain it briefly. Verify with targeted build/lint/tests or screenshot-relevant checks when possible."
|
|
301
301
|
]
|
|
302
302
|
},
|
|
303
|
+
"browser-qa": {
|
|
304
|
+
"description": "Use for browser-based visual QA: reproduce UI bugs and verify fixes with deterministic assertions, screenshots, video, and traces.",
|
|
305
|
+
"model": "openai-codex/gpt-5.4-mini",
|
|
306
|
+
"fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"],
|
|
307
|
+
"thinking": "medium",
|
|
308
|
+
"tools": ["read", "grep", "bash"]
|
|
309
|
+
},
|
|
303
310
|
"tests": {
|
|
304
311
|
"description": "Use for tests: locate coverage, find gaps, run/check targeted test commands, diagnose failing tests.",
|
|
305
312
|
"model": "zai/glm-5-turbo",
|
package/package.json
CHANGED