stageflow 0.27.0 → 0.30.0
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/README.md +4 -0
- package/builtin-skills/browser/SKILL.md +42 -0
- package/dist/agent/claudeAdapter.js +2 -2
- package/dist/agent/cursorProvider.d.ts +2 -0
- package/dist/agent/cursorProvider.js +30 -5
- package/dist/agent/fakeAgent.d.ts +1 -0
- package/dist/agent/fakeAgent.js +13 -0
- package/dist/agent/piAdapter.d.ts +3 -24
- package/dist/agent/piAdapter.js +49 -47
- package/dist/agent/piSessionFactory.d.ts +38 -0
- package/dist/agent/piSessionFactory.js +60 -0
- package/dist/agent/port.d.ts +2 -0
- package/dist/agent/streamLogRedact.d.ts +6 -0
- package/dist/agent/streamLogRedact.js +23 -1
- package/dist/browser/anchor.d.ts +27 -0
- package/dist/browser/anchor.js +127 -0
- package/dist/browser/auditSink.d.ts +38 -0
- package/dist/browser/auditSink.js +34 -0
- package/dist/browser/browserHost.d.ts +86 -0
- package/dist/browser/browserHost.js +7 -0
- package/dist/browser/browserSweep.d.ts +14 -0
- package/dist/browser/browserSweep.js +54 -0
- package/dist/browser/browserTeardown.d.ts +56 -0
- package/dist/browser/browserTeardown.js +235 -0
- package/dist/browser/fakeRemoteBrowserHost.d.ts +2 -0
- package/dist/browser/fakeRemoteBrowserHost.js +34 -0
- package/dist/browser/gateHandoff.d.ts +25 -0
- package/dist/browser/gateHandoff.js +83 -0
- package/dist/browser/humanLogin.d.ts +18 -0
- package/dist/browser/humanLogin.js +34 -0
- package/dist/browser/keyProvider.d.ts +10 -0
- package/dist/browser/keyProvider.js +39 -0
- package/dist/browser/localBrowserHost.d.ts +10 -0
- package/dist/browser/localBrowserHost.js +152 -0
- package/dist/browser/localProfileLock.d.ts +2 -0
- package/dist/browser/localProfileLock.js +218 -0
- package/dist/browser/localProfileStore.d.ts +5 -0
- package/dist/browser/localProfileStore.js +91 -0
- package/dist/browser/loginCheck.d.ts +53 -0
- package/dist/browser/loginCheck.js +147 -0
- package/dist/browser/memoryProfileLock.d.ts +2 -0
- package/dist/browser/memoryProfileLock.js +53 -0
- package/dist/browser/memoryProfileStore.d.ts +2 -0
- package/dist/browser/memoryProfileStore.js +31 -0
- package/dist/browser/navigationAudit.d.ts +16 -0
- package/dist/browser/navigationAudit.js +53 -0
- package/dist/browser/persistedEnv.d.ts +3 -0
- package/dist/browser/persistedEnv.js +15 -0
- package/dist/browser/profileLock.d.ts +40 -0
- package/dist/browser/profileLock.js +10 -0
- package/dist/browser/profileStore.d.ts +23 -0
- package/dist/browser/profileStore.js +39 -0
- package/dist/browser/runLiveness.d.ts +13 -0
- package/dist/browser/runLiveness.js +39 -0
- package/dist/browser/sitePolicy.d.ts +10 -0
- package/dist/browser/sitePolicy.js +61 -0
- package/dist/browser/stageBrowserEnv.d.ts +22 -0
- package/dist/browser/stageBrowserEnv.js +147 -0
- package/dist/browser/stageProfileLock.d.ts +17 -0
- package/dist/browser/stageProfileLock.js +39 -0
- package/dist/cli/browserCommand.d.ts +44 -0
- package/dist/cli/browserCommand.js +472 -0
- package/dist/cli/hostClient.d.ts +20 -0
- package/dist/cli/hostClient.js +36 -2
- package/dist/cli/initCommand.d.ts +1 -0
- package/dist/cli/initCommand.js +23 -7
- package/dist/cli/initTemplates.d.ts +5 -2
- package/dist/cli/initTemplates.js +25 -2
- package/dist/cli/runsCommand.js +6 -2
- package/dist/cli/triggerCommand.d.ts +12 -0
- package/dist/cli/triggerCommand.js +183 -0
- package/dist/cli/validateCommand.d.ts +1 -1
- package/dist/cli/validateCommand.js +23 -6
- package/dist/cli.js +31 -2
- package/dist/config/browseCatalog.d.ts +1 -0
- package/dist/config/browseCatalog.js +5 -4
- package/dist/config/builtinSkills.d.ts +4 -0
- package/dist/config/builtinSkills.js +23 -0
- package/dist/config/catalogRelativePath.d.ts +1 -0
- package/dist/config/catalogRelativePath.js +10 -2
- package/dist/config/createTrigger.d.ts +27 -0
- package/dist/config/createTrigger.js +232 -0
- package/dist/config/draftPackage.d.ts +117 -0
- package/dist/config/draftPackage.js +550 -0
- package/dist/config/ensureCatalogScanRoot.d.ts +16 -0
- package/dist/config/ensureCatalogScanRoot.js +73 -0
- package/dist/config/hostConfig.d.ts +2 -0
- package/dist/config/hostConfig.js +27 -0
- package/dist/config/listSkills.d.ts +2 -2
- package/dist/config/listSkills.js +17 -3
- package/dist/config/loadOutcome.d.ts +6 -0
- package/dist/config/loadPipeline.js +16 -0
- package/dist/config/loadStage.js +7 -0
- package/dist/config/loadStageflowManifest.js +25 -0
- package/dist/config/loadTrigger.d.ts +9 -0
- package/dist/config/loadTrigger.js +120 -0
- package/dist/config/multiProjectCatalog.d.ts +2 -0
- package/dist/config/multiProjectCatalog.js +26 -18
- package/dist/config/normalizePipelineStageEntry.js +8 -1
- package/dist/config/parseCompletionContract.js +6 -0
- package/dist/config/pipelineStageKeys.js +1 -0
- package/dist/config/publishDraftPackage.d.ts +7 -0
- package/dist/config/publishDraftPackage.js +25 -0
- package/dist/config/resolveCatalogContext.js +1 -1
- package/dist/config/resolvePipelineDag.js +1 -1
- package/dist/config/scanCatalogPaths.d.ts +1 -1
- package/dist/config/scanCatalogPaths.js +6 -2
- package/dist/config/stageBrowser.d.ts +12 -0
- package/dist/config/stageBrowser.js +118 -0
- package/dist/config/triggerCatalog.d.ts +10 -0
- package/dist/config/triggerCatalog.js +37 -0
- package/dist/config/validateCatalog.d.ts +13 -3
- package/dist/config/validateCatalog.js +83 -2
- package/dist/config/validateCompletionContract.js +8 -0
- package/dist/envelope/preEmitChecks.d.ts +1 -1
- package/dist/envelope/preEmitChecks.js +8 -0
- package/dist/logging/redact.d.ts +4 -0
- package/dist/logging/redact.js +17 -0
- package/dist/mcp/catalogTools.js +43 -8
- package/dist/mcp/controlTools.js +3 -1
- package/dist/mcp/deps.d.ts +2 -0
- package/dist/mcp/server.js +1 -0
- package/dist/mcp/tools.js +2 -0
- package/dist/mcp/triggerTools.d.ts +3 -0
- package/dist/mcp/triggerTools.js +176 -0
- package/dist/operatorAgent/docsRetrieval.d.ts +40 -0
- package/dist/operatorAgent/docsRetrieval.js +269 -0
- package/dist/operatorAgent/draftContext.d.ts +16 -0
- package/dist/operatorAgent/draftContext.js +68 -0
- package/dist/operatorAgent/fakeHost.d.ts +39 -0
- package/dist/operatorAgent/fakeHost.js +92 -0
- package/dist/operatorAgent/host.d.ts +40 -0
- package/dist/operatorAgent/host.js +179 -0
- package/dist/operatorAgent/index.d.ts +9 -0
- package/dist/operatorAgent/index.js +8 -0
- package/dist/operatorAgent/piModel.d.ts +80 -0
- package/dist/operatorAgent/piModel.js +528 -0
- package/dist/operatorAgent/profiles/workshopAuthor.d.ts +23 -0
- package/dist/operatorAgent/profiles/workshopAuthor.js +338 -0
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +6 -0
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +103 -0
- package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +57 -0
- package/dist/operatorAgent/profiles/workshopAuthorTools.js +623 -0
- package/dist/operatorAgent/proposals.d.ts +32 -0
- package/dist/operatorAgent/proposals.js +144 -0
- package/dist/operatorAgent/types.d.ts +128 -0
- package/dist/operatorAgent/types.js +1 -0
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/project/findManifestRoot.d.ts +3 -0
- package/dist/project/findManifestRoot.js +35 -0
- package/dist/project/resolveStageflowContext.d.ts +7 -0
- package/dist/project/resolveStageflowContext.js +44 -28
- package/dist/runstore/assertTmpdir.js +6 -2
- package/dist/runstore/port.d.ts +28 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +8 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +90 -0
- package/dist/runstore/sqlite/migrations/008-triggers-table.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/008-triggers-table.js +19 -0
- package/dist/runstore/sqlite/migrations/009-trigger-next-run.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/009-trigger-next-run.js +19 -0
- package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.js +17 -0
- package/dist/runstore/sqlite/migrations/index.js +6 -0
- package/dist/runtime/answerResume.d.ts +1 -0
- package/dist/runtime/answerResume.js +3 -1
- package/dist/runtime/completionCheckRunner.d.ts +12 -1
- package/dist/runtime/completionCheckRunner.js +22 -0
- package/dist/runtime/credentialBinding.d.ts +2 -0
- package/dist/runtime/credentialBinding.js +3 -0
- package/dist/runtime/emailSource.d.ts +72 -0
- package/dist/runtime/emailSource.js +304 -0
- package/dist/runtime/githubPollSource.d.ts +86 -0
- package/dist/runtime/githubPollSource.js +284 -0
- package/dist/runtime/pipelineRunner.d.ts +4 -0
- package/dist/runtime/pipelineRunner.js +3 -0
- package/dist/runtime/pipelineScheduler.d.ts +2 -0
- package/dist/runtime/pipelineScheduler.js +85 -1
- package/dist/runtime/repositoryMaterialize.d.ts +1 -1
- package/dist/runtime/runManager.d.ts +11 -0
- package/dist/runtime/runManager.js +149 -1
- package/dist/runtime/runRetryCoordinator.d.ts +2 -0
- package/dist/runtime/runRetryCoordinator.js +1 -0
- package/dist/runtime/scheduleSource.d.ts +38 -0
- package/dist/runtime/scheduleSource.js +119 -0
- package/dist/runtime/settingsFile.d.ts +5 -0
- package/dist/runtime/settingsFile.js +61 -54
- package/dist/runtime/stageAttemptBootstrap.js +55 -3
- package/dist/runtime/stageEnvironment.js +1 -0
- package/dist/runtime/stageHitl.d.ts +1 -0
- package/dist/runtime/stageHitl.js +5 -1
- package/dist/runtime/stageProcessLauncher.d.ts +2 -0
- package/dist/runtime/stageProcessLauncher.js +44 -5
- package/dist/runtime/stageRunner.d.ts +5 -0
- package/dist/runtime/stageRunner.js +30 -6
- package/dist/runtime/stageStreamLog.d.ts +1 -0
- package/dist/runtime/stageStreamLog.js +2 -1
- package/dist/runtime/triggerCatalog.d.ts +6 -0
- package/dist/runtime/triggerCatalog.js +24 -0
- package/dist/runtime/triggerEventMatch.d.ts +2 -0
- package/dist/runtime/triggerEventMatch.js +6 -0
- package/dist/runtime/triggerPort.d.ts +14 -0
- package/dist/runtime/triggerPort.js +1 -0
- package/dist/runtime/triggerRunner.d.ts +17 -0
- package/dist/runtime/triggerRunner.js +89 -0
- package/dist/runtime/verifiedStageExecution.d.ts +7 -0
- package/dist/runtime/verifiedStageExecution.js +14 -0
- package/dist/runtime/webhookSignature.d.ts +1 -0
- package/dist/runtime/webhookSignature.js +12 -0
- package/dist/server/bootstrap.d.ts +6 -0
- package/dist/server/bootstrap.js +64 -0
- package/dist/server/controlToken.js +13 -0
- package/dist/server/createHttpHost.js +3 -0
- package/dist/server/healthSurfaces.js +31 -6
- package/dist/server/http.d.ts +15 -0
- package/dist/server/http.js +1251 -74
- package/dist/server/requestAuthContext.d.ts +1 -1
- package/dist/server/triggerRoutes.d.ts +15 -0
- package/dist/server/triggerRoutes.js +268 -0
- package/dist/tools/askOperator.d.ts +3 -2
- package/dist/types/completion.d.ts +3 -0
- package/dist/types/pipeline.d.ts +3 -1
- package/dist/types/preEmitCheck.d.ts +7 -0
- package/dist/types/stage.d.ts +14 -0
- package/dist/types/stageflowManifest.d.ts +3 -0
- package/dist/types/trigger.d.ts +18 -0
- package/dist/types/trigger.js +1 -0
- package/dist/ui/assets/{index-CSsDued1.css → index-Bv4p1aPK.css} +1 -1
- package/dist/ui/assets/index-CntB4bZR.js +133 -0
- package/dist/ui/index.html +2 -2
- package/dist/workshop/autosave.d.ts +58 -0
- package/dist/workshop/autosave.js +222 -0
- package/dist/workshop/buildStore.d.ts +79 -0
- package/dist/workshop/buildStore.js +326 -0
- package/dist/workshop/chatTurn.d.ts +232 -0
- package/dist/workshop/chatTurn.js +475 -0
- package/dist/workshop/modelSettings.d.ts +18 -0
- package/dist/workshop/modelSettings.js +44 -0
- package/dist/workshop/sessionStore.d.ts +61 -0
- package/dist/workshop/sessionStore.js +254 -0
- package/dist/workshop/workshopPin.d.ts +72 -0
- package/dist/workshop/workshopPin.js +193 -0
- package/examples/README.md +2 -1
- package/examples/browser-session/README.md +77 -0
- package/examples/browser-session/browser-session.pipeline.yaml +50 -0
- package/examples/browser-session/browser-session.task.yaml +6 -0
- package/examples/browser-session/check-login.yaml +22 -0
- package/examples/browser-session/fixture-server.mjs +64 -0
- package/examples/browser-session/human-login.yaml +31 -0
- package/examples/browser-session/merge.yaml +36 -0
- package/examples/browser-session/work-a.yaml +41 -0
- package/examples/browser-session/work-b.yaml +41 -0
- package/examples/stage-mcp/README.md +3 -2
- package/examples/stageflow.yaml +51 -0
- package/package.json +5 -1
- package/skills/stageflow/references/control-surface.md +3 -1
- package/skills/stageflow-author/references/stage-prompt-template.md +4 -4
- package/skills/stageflow-run/SKILL.md +2 -0
- package/skills/stageflow-run/references/task-and-pipeline-selection.md +3 -1
- package/skills/stageflow-setup/SKILL.md +11 -0
- package/dist/ui/assets/index-B6hlSXXn.js +0 -120
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export class BlockedSiteError extends Error {
|
|
2
|
+
code = "browser_site_blocked";
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "BlockedSiteError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function normalizeDomain(raw) {
|
|
9
|
+
return raw
|
|
10
|
+
.trim()
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.replace(/^\*\./, "")
|
|
13
|
+
.replace(/^\./, "")
|
|
14
|
+
.replace(/\.$/, "");
|
|
15
|
+
}
|
|
16
|
+
export function hostOf(urlOrHost) {
|
|
17
|
+
const text = urlOrHost.trim();
|
|
18
|
+
if (text.length === 0)
|
|
19
|
+
return undefined;
|
|
20
|
+
try {
|
|
21
|
+
const parsed = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(text) ? text : `https://${text}`);
|
|
22
|
+
return parsed.hostname.toLowerCase().replace(/\.$/, "") || undefined;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function within(host, domain) {
|
|
29
|
+
return host === domain || host.endsWith(`.${domain}`);
|
|
30
|
+
}
|
|
31
|
+
export function isBlockedHost(host, blocked) {
|
|
32
|
+
const h = normalizeDomain(host);
|
|
33
|
+
return blocked.map(normalizeDomain).find((d) => d.length > 0 && within(h, d));
|
|
34
|
+
}
|
|
35
|
+
/** Throws when the stage's allow_domains or check URLs touch a Host-blocked site. */
|
|
36
|
+
export function assertBrowserSitesAllowed(stageId, browser, blocked) {
|
|
37
|
+
if (blocked.length === 0)
|
|
38
|
+
return;
|
|
39
|
+
const norm = blocked.map(normalizeDomain).filter((d) => d.length > 0);
|
|
40
|
+
const fail = (site, where) => {
|
|
41
|
+
throw new BlockedSiteError(`Stage "${stageId}": browser ${where} "${site}" is blocked by Host policy (browser.blocked_sites)`);
|
|
42
|
+
};
|
|
43
|
+
for (const entry of browser.allow_domains ?? []) {
|
|
44
|
+
const d = normalizeDomain(entry);
|
|
45
|
+
if (norm.some((b) => within(d, b) || within(b, d)))
|
|
46
|
+
fail(entry, "allow_domains entry");
|
|
47
|
+
}
|
|
48
|
+
const urls = [
|
|
49
|
+
["login_url", browser.login_url],
|
|
50
|
+
["check.url", browser.check?.url],
|
|
51
|
+
["check.logged_in_url", browser.check?.logged_in_url],
|
|
52
|
+
...[browser.check?.logged_out_url].flat().map((u) => ["check.logged_out_url", u]),
|
|
53
|
+
];
|
|
54
|
+
for (const [where, url] of urls) {
|
|
55
|
+
if (!url)
|
|
56
|
+
continue;
|
|
57
|
+
const host = hostOf(url);
|
|
58
|
+
if (host !== undefined && isBlockedHost(host, norm) !== undefined)
|
|
59
|
+
fail(host, where);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { StageBrowserConfig } from "../types/stage.js";
|
|
2
|
+
import { BROWSER_ENV_FILENAME, type BrowserEnv, type StageBrowserSupport } from "./browserHost.js";
|
|
3
|
+
export { BROWSER_ENV_FILENAME };
|
|
4
|
+
export declare function readStagePersistedBrowserEnv(runDir: string, stageId: string): Promise<BrowserEnv | undefined>;
|
|
5
|
+
export declare function defaultStageBrowserSupport(): StageBrowserSupport;
|
|
6
|
+
/**
|
|
7
|
+
* Computed once per stage and persisted in the run dir: agent-browser silently
|
|
8
|
+
* relaunches the browser when a command's launch env differs from the daemon's,
|
|
9
|
+
* so every attempt and resume worker must see byte-identical values.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveStageBrowserEnv(support: StageBrowserSupport, input: {
|
|
12
|
+
runId: string;
|
|
13
|
+
stageId: string;
|
|
14
|
+
runDir: string;
|
|
15
|
+
browser: StageBrowserConfig | undefined;
|
|
16
|
+
/** Attempt the login check result belongs to; defaults to 1. */
|
|
17
|
+
attempt?: number;
|
|
18
|
+
/** Human login stage: needs a screen, opens the login page, skips the pre-agent check. */
|
|
19
|
+
humanLogin?: boolean;
|
|
20
|
+
/** Resume worker: the operator's window is already open; do not navigate it. */
|
|
21
|
+
resuming?: boolean;
|
|
22
|
+
}): Promise<BrowserEnv | undefined>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { loadHostConfig } from "../config/hostConfig.js";
|
|
4
|
+
import { stageDir } from "../runstore/paths.js";
|
|
5
|
+
import { safeAudit } from "./auditSink.js";
|
|
6
|
+
import { assertBrowserSitesAllowed } from "./sitePolicy.js";
|
|
7
|
+
import { BROWSER_ENV_FILENAME, BROWSER_POLICY_FILENAME, } from "./browserHost.js";
|
|
8
|
+
import { ensureRunProfileBrowser } from "./anchor.js";
|
|
9
|
+
import { BROWSER_CLOSED_FILENAME, closeBrowserSession, defaultBrowserRunner, writeSessionOwner, } from "./browserTeardown.js";
|
|
10
|
+
import { defaultDisplayProbe, loginPageUrl, noScreenError } from "./humanLogin.js";
|
|
11
|
+
import { ensureStageLoginCheck, OPEN_COMMAND_TIMEOUT_MS } from "./loginCheck.js";
|
|
12
|
+
import { readPersistedBrowserEnv } from "./persistedEnv.js";
|
|
13
|
+
import { createLocalBrowserHost } from "./localBrowserHost.js";
|
|
14
|
+
import { createLocalProfileStore } from "./localProfileStore.js";
|
|
15
|
+
import { LOCAL_BROWSER_SCOPE } from "./profileStore.js";
|
|
16
|
+
export { BROWSER_ENV_FILENAME };
|
|
17
|
+
export async function readStagePersistedBrowserEnv(runDir, stageId) {
|
|
18
|
+
return readPersistedBrowserEnv(path.join(stageDir(runDir, stageId), BROWSER_ENV_FILENAME));
|
|
19
|
+
}
|
|
20
|
+
let defaultSupport;
|
|
21
|
+
export function defaultStageBrowserSupport() {
|
|
22
|
+
defaultSupport ??= {
|
|
23
|
+
host: createLocalBrowserHost(),
|
|
24
|
+
profiles: createLocalProfileStore(),
|
|
25
|
+
};
|
|
26
|
+
return defaultSupport;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Computed once per stage and persisted in the run dir: agent-browser silently
|
|
30
|
+
* relaunches the browser when a command's launch env differs from the daemon's,
|
|
31
|
+
* so every attempt and resume worker must see byte-identical values.
|
|
32
|
+
*/
|
|
33
|
+
export async function resolveStageBrowserEnv(support, input) {
|
|
34
|
+
const { browser } = input;
|
|
35
|
+
if (browser === undefined)
|
|
36
|
+
return undefined;
|
|
37
|
+
if (input.humanLogin === true) {
|
|
38
|
+
const screen = (support.display ?? defaultDisplayProbe)();
|
|
39
|
+
if (!screen.hasDisplay)
|
|
40
|
+
throw noScreenError(screen.docker);
|
|
41
|
+
}
|
|
42
|
+
if ((browser.allow_domains?.length ?? 0) > 0 ||
|
|
43
|
+
browser.check !== undefined ||
|
|
44
|
+
browser.login_url !== undefined) {
|
|
45
|
+
assertBrowserSitesAllowed(input.stageId, browser, support.blockedSites ?? loadHostConfig().browserBlockedSites);
|
|
46
|
+
}
|
|
47
|
+
const dir = stageDir(input.runDir, input.stageId);
|
|
48
|
+
const file = path.join(dir, BROWSER_ENV_FILENAME);
|
|
49
|
+
// TODO(multi-tenant): open the profile in the run owner's scope, not the fixed local scope.
|
|
50
|
+
const profile = browser.profile !== undefined
|
|
51
|
+
? await support.profiles.open({
|
|
52
|
+
scope: LOCAL_BROWSER_SCOPE,
|
|
53
|
+
name: browser.profile,
|
|
54
|
+
})
|
|
55
|
+
: undefined;
|
|
56
|
+
const anchor = profile !== undefined
|
|
57
|
+
? await ensureRunProfileBrowser(support, {
|
|
58
|
+
runId: input.runId,
|
|
59
|
+
runDir: input.runDir,
|
|
60
|
+
browser,
|
|
61
|
+
profile,
|
|
62
|
+
...(input.humanLogin === true ? { humanLogin: true } : {}),
|
|
63
|
+
})
|
|
64
|
+
: undefined;
|
|
65
|
+
const fresh = await support.host.stageEnv({
|
|
66
|
+
runId: input.runId,
|
|
67
|
+
stageId: input.stageId,
|
|
68
|
+
browser,
|
|
69
|
+
...(profile !== undefined ? { profile } : {}),
|
|
70
|
+
...(anchor !== undefined ? { cdpAddress: anchor.cdpAddress } : {}),
|
|
71
|
+
...(input.humanLogin === true ? { humanLogin: true } : {}),
|
|
72
|
+
});
|
|
73
|
+
await rm(path.join(dir, BROWSER_CLOSED_FILENAME), { force: true });
|
|
74
|
+
const persisted = await readPersistedBrowserEnv(file);
|
|
75
|
+
let env = persisted ?? fresh;
|
|
76
|
+
const writeEnv = async (value) => {
|
|
77
|
+
await mkdir(dir, { recursive: true });
|
|
78
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
79
|
+
await writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
80
|
+
await rename(tmp, file);
|
|
81
|
+
};
|
|
82
|
+
if (persisted !== undefined &&
|
|
83
|
+
anchor !== undefined &&
|
|
84
|
+
persisted.AGENT_BROWSER_CDP !== anchor.cdpAddress) {
|
|
85
|
+
// The shared browser was restarted: only the attach address changes. The
|
|
86
|
+
// old stage daemon is still attached to the dead address, so close it first.
|
|
87
|
+
await closeBrowserSession(persisted, {
|
|
88
|
+
...(support.runner !== undefined ? { runner: support.runner } : {}),
|
|
89
|
+
...(support.closeWaitMs !== undefined ? { closeWaitMs: support.closeWaitMs } : {}),
|
|
90
|
+
}).catch(() => undefined);
|
|
91
|
+
if (persisted.AGENT_BROWSER_SOCKET_DIR !== undefined) {
|
|
92
|
+
await mkdir(persisted.AGENT_BROWSER_SOCKET_DIR, { recursive: true, mode: 0o700 });
|
|
93
|
+
}
|
|
94
|
+
env = { ...persisted, AGENT_BROWSER_CDP: anchor.cdpAddress };
|
|
95
|
+
await writeEnv(env);
|
|
96
|
+
}
|
|
97
|
+
if (persisted === undefined) {
|
|
98
|
+
await writeEnv(fresh);
|
|
99
|
+
if (browser.profile !== undefined) {
|
|
100
|
+
// TODO(multi-tenant): audit the run owner's scope.
|
|
101
|
+
await safeAudit(support.audit, {
|
|
102
|
+
event: "profile_used",
|
|
103
|
+
scope: LOCAL_BROWSER_SCOPE,
|
|
104
|
+
profile: browser.profile,
|
|
105
|
+
runId: input.runId,
|
|
106
|
+
stageId: input.stageId,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (browser.profile !== undefined && (browser.allow_domains?.length ?? 0) > 0) {
|
|
111
|
+
await writeFile(path.join(dir, BROWSER_POLICY_FILENAME), `${JSON.stringify({
|
|
112
|
+
runId: input.runId,
|
|
113
|
+
stageId: input.stageId,
|
|
114
|
+
profile: browser.profile,
|
|
115
|
+
allow_domains: browser.allow_domains,
|
|
116
|
+
})}\n`, { mode: 0o600 });
|
|
117
|
+
}
|
|
118
|
+
await writeSessionOwner({
|
|
119
|
+
runId: input.runId,
|
|
120
|
+
stageId: input.stageId,
|
|
121
|
+
runDir: input.runDir,
|
|
122
|
+
env,
|
|
123
|
+
}).catch(() => undefined);
|
|
124
|
+
if (input.humanLogin === true) {
|
|
125
|
+
const url = input.resuming === true ? undefined : loginPageUrl(browser);
|
|
126
|
+
if (url !== undefined) {
|
|
127
|
+
const opened = await (support.runner ?? defaultBrowserRunner)(["open", url], env, {
|
|
128
|
+
timeoutMs: OPEN_COMMAND_TIMEOUT_MS,
|
|
129
|
+
});
|
|
130
|
+
if (opened.code !== 0) {
|
|
131
|
+
throw new Error(`could not open the login page ${url}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (browser.check !== undefined) {
|
|
136
|
+
await ensureStageLoginCheck({
|
|
137
|
+
runDir: input.runDir,
|
|
138
|
+
stageId: input.stageId,
|
|
139
|
+
attempt: input.attempt ?? 1,
|
|
140
|
+
env,
|
|
141
|
+
check: browser.check,
|
|
142
|
+
...(support.runner !== undefined ? { runner: support.runner } : {}),
|
|
143
|
+
...(support.loginCheck !== undefined ? { options: support.loginCheck } : {}),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return env;
|
|
147
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { StageBrowserSupport } from "./browserHost.js";
|
|
2
|
+
import type { ProfileLock, ProfileLockOwner, RunLiveness } from "./profileLock.js";
|
|
3
|
+
export declare function stageProfileLock(support: StageBrowserSupport): ProfileLock;
|
|
4
|
+
export type ProfileWaitInput = {
|
|
5
|
+
profile: string;
|
|
6
|
+
owner: ProfileLockOwner;
|
|
7
|
+
isRunLive?: RunLiveness;
|
|
8
|
+
halted?: () => boolean;
|
|
9
|
+
/** Called when the holder is first seen or changes. */
|
|
10
|
+
onWaiting?: (holder: ProfileLockOwner) => Promise<void> | void;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Joins the run's lease on the profile, or takes it, waiting while another run
|
|
14
|
+
* holds it. The lease is released at run end, never at stage end.
|
|
15
|
+
*/
|
|
16
|
+
export declare function acquireStageProfile(support: StageBrowserSupport, input: ProfileWaitInput): Promise<"acquired" | "halted">;
|
|
17
|
+
export declare function profileWaitingMessage(profile: string, holder: ProfileLockOwner): string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createLocalProfileLock } from "./localProfileLock.js";
|
|
2
|
+
import { LOCAL_BROWSER_SCOPE } from "./profileStore.js";
|
|
3
|
+
const DEFAULT_LOCK_POLL_MS = 250;
|
|
4
|
+
let defaultLock;
|
|
5
|
+
export function stageProfileLock(support) {
|
|
6
|
+
if (support.locks !== undefined)
|
|
7
|
+
return support.locks;
|
|
8
|
+
defaultLock ??= createLocalProfileLock();
|
|
9
|
+
return defaultLock;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Joins the run's lease on the profile, or takes it, waiting while another run
|
|
13
|
+
* holds it. The lease is released at run end, never at stage end.
|
|
14
|
+
*/
|
|
15
|
+
export async function acquireStageProfile(support, input) {
|
|
16
|
+
const locks = stageProfileLock(support);
|
|
17
|
+
// TODO(multi-tenant): take the scope from the run owner instead of the fixed local scope.
|
|
18
|
+
const key = { scope: LOCAL_BROWSER_SCOPE, name: input.profile };
|
|
19
|
+
const pollMs = support.lockPollMs ?? DEFAULT_LOCK_POLL_MS;
|
|
20
|
+
let announced;
|
|
21
|
+
for (;;) {
|
|
22
|
+
const result = await locks.acquire(key, input.owner);
|
|
23
|
+
if (result.status === "acquired")
|
|
24
|
+
return "acquired";
|
|
25
|
+
if (input.halted?.())
|
|
26
|
+
return "halted";
|
|
27
|
+
if (announced === undefined || announced.runId !== result.holder.runId) {
|
|
28
|
+
announced = result.holder;
|
|
29
|
+
await input.onWaiting?.(result.holder);
|
|
30
|
+
}
|
|
31
|
+
if (input.isRunLive !== undefined) {
|
|
32
|
+
await locks.reclaimStale(input.isRunLive).catch(() => 0);
|
|
33
|
+
}
|
|
34
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function profileWaitingMessage(profile, holder) {
|
|
38
|
+
return `waiting for browser profile "${profile}" held by run ${holder.runId}`;
|
|
39
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type AuditSink } from "../browser/auditSink.js";
|
|
2
|
+
import type { BrowserEnv, BrowserHost, BrowserRunner } from "../browser/browserHost.js";
|
|
3
|
+
import { type DisplayProbe } from "../browser/humanLogin.js";
|
|
4
|
+
import type { ProfileLock, RunLiveness } from "../browser/profileLock.js";
|
|
5
|
+
import { type ProfileStore } from "../browser/profileStore.js";
|
|
6
|
+
export declare const BROWSER_USAGE = "Usage:\n sf browser profiles [--json]\n sf browser status <name> [--json]\n sf browser check <name> --url <url> --logged-in <glob> [--logged-out <glob>]... [--headless] [--json]\n sf browser login <name> --url <login-url> --logged-in <glob> [--timeout-sec <n>] [--json]\n sf browser clear <name> [--yes] [--json]\n\nManage saved browser logins (profiles) on this computer. Output never shows profile paths or cookie values.\nExit codes: 0 ok / logged in; 1 error; 2 profile in use by a live run; 3 logged out; 4 unknown; 5 login not completed (timeout or window closed); 130 interrupted.";
|
|
7
|
+
export declare const BROWSER_EXIT: {
|
|
8
|
+
readonly ok: 0;
|
|
9
|
+
readonly error: 1;
|
|
10
|
+
readonly busy: 2;
|
|
11
|
+
readonly loggedOut: 3;
|
|
12
|
+
readonly unknown: 4;
|
|
13
|
+
readonly notCompleted: 5;
|
|
14
|
+
readonly interrupted: 130;
|
|
15
|
+
};
|
|
16
|
+
export type BrowserCommandDeps = {
|
|
17
|
+
log: (line: string) => void;
|
|
18
|
+
error: (line: string) => void;
|
|
19
|
+
store: ProfileStore;
|
|
20
|
+
host: BrowserHost;
|
|
21
|
+
locks: ProfileLock;
|
|
22
|
+
runner: BrowserRunner;
|
|
23
|
+
audit?: AuditSink;
|
|
24
|
+
/** Host-blocked sites; defaults to `browser.blocked_sites` from Host config. */
|
|
25
|
+
blockedSites?: readonly string[];
|
|
26
|
+
display: DisplayProbe;
|
|
27
|
+
/** Liveness of the run holding a profile lock. */
|
|
28
|
+
isRunLive: RunLiveness;
|
|
29
|
+
/** True when a daemon session for this env is open. */
|
|
30
|
+
sessionOpen: (env: BrowserEnv) => boolean;
|
|
31
|
+
/** profile name -> ISO time of last recorded use (from the audit log). */
|
|
32
|
+
auditLastUsed: () => Promise<Record<string, string>>;
|
|
33
|
+
confirm: (question: string) => Promise<boolean>;
|
|
34
|
+
interactive: boolean;
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
sleep: (ms: number) => Promise<void>;
|
|
37
|
+
closeWaitMs?: number;
|
|
38
|
+
loginCheck?: {
|
|
39
|
+
waitMs?: number;
|
|
40
|
+
settleMs?: number;
|
|
41
|
+
};
|
|
42
|
+
loginPollMs?: number;
|
|
43
|
+
};
|
|
44
|
+
export declare function runBrowserCommand(args: string[], deps?: Partial<BrowserCommandDeps>): Promise<number>;
|