stageflow 0.27.0 → 0.31.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 +10 -0
- package/dist/agent/cursorProvider.js +83 -8
- 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/providerAuth.d.ts +7 -3
- package/dist/agent/providerAuth.js +5 -3
- 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/doctorCommand.js +7 -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/providersCommand.d.ts +1 -1
- package/dist/cli/providersCommand.js +4 -4
- 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 +32 -3
- 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/diskUsage.d.ts +1 -1
- package/dist/runstore/diskUsage.js +2 -7
- 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 +7 -2
- package/dist/runtime/credentialBinding.js +20 -28
- 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 +86 -1
- package/dist/runtime/repositoryMaterialize.d.ts +1 -1
- package/dist/runtime/runManager.d.ts +11 -0
- package/dist/runtime/runManager.js +161 -3
- 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 +7 -2
- package/dist/runtime/settingsFile.js +66 -56
- 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 +3 -0
- package/dist/runtime/stageProcessLauncher.js +52 -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 +70 -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-p8Iou3zk.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/conditional-fork/decide.yaml +1 -1
- package/examples/github-release/README.md +17 -3
- package/examples/stage-mcp/README.md +3 -2
- package/examples/stageflow.yaml +51 -0
- package/package.json +9 -4
- 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
|
@@ -25,12 +25,16 @@ export type ProvidersListResult = {
|
|
|
25
25
|
via: "pi";
|
|
26
26
|
providers: ProviderSummary[];
|
|
27
27
|
};
|
|
28
|
-
export type
|
|
29
|
-
piHomeUsable: boolean;
|
|
28
|
+
export type ProvidersDetectResult = {
|
|
30
29
|
credentialSource?: CredentialSource;
|
|
31
30
|
provisional: boolean;
|
|
32
31
|
source: CredentialSource;
|
|
32
|
+
authConfigured: boolean;
|
|
33
|
+
cursorSdkReady: boolean;
|
|
34
|
+
cursorApiKeyConfigured: boolean;
|
|
33
35
|
};
|
|
36
|
+
/** @deprecated Use ProvidersDetectResult */
|
|
37
|
+
export type PiHomeDetectResult = ProvidersDetectResult;
|
|
34
38
|
export type ProviderAuthRuntime = {
|
|
35
39
|
getProviders(): readonly Provider[];
|
|
36
40
|
getProvider(providerId: string): Provider | undefined;
|
|
@@ -84,7 +88,7 @@ export type OauthLoginResult = {
|
|
|
84
88
|
};
|
|
85
89
|
export declare function loginWithOauth(cwd: string, providerId: string, interaction: AuthInteraction, ctx?: ProviderAuthContext): Promise<OauthLoginResult>;
|
|
86
90
|
export declare function logoutProvider(cwd: string, providerId: string, ctx?: ProviderAuthContext): Promise<ProviderAuthStatus>;
|
|
87
|
-
export declare function detectPiHome(cwd: string):
|
|
91
|
+
export declare function detectPiHome(cwd: string): ProvidersDetectResult;
|
|
88
92
|
export declare function getCredentialSourceSettings(cwd: string): {
|
|
89
93
|
credentialSource?: CredentialSource;
|
|
90
94
|
binding: {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CredentialSynchronizationError, ModelRuntime, } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
2
|
+
import { parseCredentialSource, resolveCredentialBinding, writeCredentialSourceToContext, isUsableAuthFile, } from "../runtime/credentialBinding.js";
|
|
3
|
+
import { getCursorHostStatus } from "./cursorProvider.js";
|
|
3
4
|
import { readPersistedCredentialSourceFromContext, } from "../runtime/settingsFile.js";
|
|
4
5
|
import { resolveProjectContext } from "../project/resolveProjectContext.js";
|
|
5
6
|
export function makeMutationLock() {
|
|
@@ -251,10 +252,11 @@ export function detectPiHome(cwd) {
|
|
|
251
252
|
const binding = resolveCredentialBinding(projectCtx);
|
|
252
253
|
const persisted = readPersistedCredentialSourceFromContext(projectCtx);
|
|
253
254
|
return {
|
|
254
|
-
piHomeUsable: isUsableAuthFile(piHomeAuthPath()),
|
|
255
255
|
...(persisted !== undefined ? { credentialSource: persisted } : {}),
|
|
256
256
|
provisional: binding.provisional,
|
|
257
257
|
source: binding.source,
|
|
258
|
+
authConfigured: isUsableAuthFile(binding.authPath),
|
|
259
|
+
...getCursorHostStatus(),
|
|
258
260
|
};
|
|
259
261
|
}
|
|
260
262
|
export function getCredentialSourceSettings(cwd) {
|
|
@@ -272,7 +274,7 @@ export function getCredentialSourceSettings(cwd) {
|
|
|
272
274
|
export function setCredentialSource(cwd, credentialSource) {
|
|
273
275
|
const parsed = parseCredentialSource(credentialSource);
|
|
274
276
|
if (parsed === undefined) {
|
|
275
|
-
throw new ProviderAuthError('credentialSource must be "
|
|
277
|
+
throw new ProviderAuthError('credentialSource must be "sf_owned"', 400);
|
|
276
278
|
}
|
|
277
279
|
const projectCtx = resolveProjectContext(cwd);
|
|
278
280
|
writeCredentialSourceToContext(projectCtx, parsed);
|
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
/** Scrub secret-shaped substrings and registered values before text is persisted. */
|
|
2
2
|
export declare function redactSecrets(text: string): string;
|
|
3
|
+
/** Like redactSecrets plus cookie/storage/header shapes; for browser stages only. */
|
|
4
|
+
export declare function redactBrowserSecrets(text: string): string;
|
|
5
|
+
/** Tool/message previews of browser stages can carry cookie or token values. */
|
|
6
|
+
export declare function redactBrowserActivityEvent<T extends {
|
|
7
|
+
event: string;
|
|
8
|
+
}>(event: T): T;
|
|
@@ -1,6 +1,28 @@
|
|
|
1
|
-
import { redactString } from "../logging/redact.js";
|
|
1
|
+
import { BROWSER_STAGE_PATTERNS, redact, redactString, } from "../logging/redact.js";
|
|
2
2
|
import { getNamedSecrets } from "../logging/namedSecrets.js";
|
|
3
3
|
/** Scrub secret-shaped substrings and registered values before text is persisted. */
|
|
4
4
|
export function redactSecrets(text) {
|
|
5
5
|
return redactString(text, { namedSecrets: getNamedSecrets() });
|
|
6
6
|
}
|
|
7
|
+
/** Like redactSecrets plus cookie/storage/header shapes; for browser stages only. */
|
|
8
|
+
export function redactBrowserSecrets(text) {
|
|
9
|
+
return redactString(text, {
|
|
10
|
+
patterns: BROWSER_STAGE_PATTERNS,
|
|
11
|
+
namedSecrets: getNamedSecrets(),
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
const BROWSER_REDACTED_EVENTS = new Set([
|
|
15
|
+
"message",
|
|
16
|
+
"tool_start",
|
|
17
|
+
"tool_end",
|
|
18
|
+
"tool_progress",
|
|
19
|
+
]);
|
|
20
|
+
/** Tool/message previews of browser stages can carry cookie or token values. */
|
|
21
|
+
export function redactBrowserActivityEvent(event) {
|
|
22
|
+
if (!BROWSER_REDACTED_EVENTS.has(event.event))
|
|
23
|
+
return event;
|
|
24
|
+
return redact(event, {
|
|
25
|
+
patterns: BROWSER_STAGE_PATTERNS,
|
|
26
|
+
namedSecrets: getNamedSecrets(),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ProfileBrowser, type ProfileBrowserResult, type StageBrowserSupport } from "./browserHost.js";
|
|
2
|
+
import type { ProfileHandle } from "./profileStore.js";
|
|
3
|
+
import type { StageBrowserConfig } from "../types/stage.js";
|
|
4
|
+
export declare const BROWSER_ANCHOR_FILENAME = "anchor.json";
|
|
5
|
+
export type PersistedAnchor = ProfileBrowser & {
|
|
6
|
+
runId: string;
|
|
7
|
+
profile: string;
|
|
8
|
+
/** Bumped each time the anchor was replaced after the first start. */
|
|
9
|
+
restarts: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function anchorDir(runDir: string, profileName: string): string;
|
|
12
|
+
export declare function readPersistedAnchor(runDir: string, profileName: string): Promise<PersistedAnchor | undefined>;
|
|
13
|
+
/**
|
|
14
|
+
* One shared browser per (run, profile). Concurrent callers in this process
|
|
15
|
+
* share one promise; other processes serialize on a lock dir. The anchor and
|
|
16
|
+
* its CDP address are persisted so stages, resume workers and the sweep reuse
|
|
17
|
+
* them; a dead anchor is restarted and the address refreshed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ensureRunProfileBrowser(support: StageBrowserSupport, input: {
|
|
20
|
+
runId: string;
|
|
21
|
+
runDir: string;
|
|
22
|
+
browser: StageBrowserConfig;
|
|
23
|
+
profile: ProfileHandle;
|
|
24
|
+
humanLogin?: boolean;
|
|
25
|
+
}): Promise<ProfileBrowserResult & {
|
|
26
|
+
persisted: PersistedAnchor;
|
|
27
|
+
}>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { defaultBrowserRunner, writeSessionOwner } from "./browserTeardown.js";
|
|
4
|
+
export const BROWSER_ANCHOR_FILENAME = "anchor.json";
|
|
5
|
+
const ANCHOR_LOCK_DIRNAME = "anchor.lock";
|
|
6
|
+
const LOCK_POLL_MS = 25;
|
|
7
|
+
const LOCK_WAIT_MS = 120_000;
|
|
8
|
+
export function anchorDir(runDir, profileName) {
|
|
9
|
+
return path.join(runDir, "browser", profileName);
|
|
10
|
+
}
|
|
11
|
+
export async function readPersistedAnchor(runDir, profileName) {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(await readFile(path.join(anchorDir(runDir, profileName), BROWSER_ANCHOR_FILENAME), "utf8"));
|
|
14
|
+
if (typeof parsed.cdpAddress === "string" &&
|
|
15
|
+
parsed.anchorEnv !== undefined &&
|
|
16
|
+
typeof parsed.anchorEnv === "object") {
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
if (err.code !== "ENOENT")
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
function pidAlive(pid) {
|
|
27
|
+
try {
|
|
28
|
+
process.kill(pid, 0);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
return err.code === "EPERM";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Cross-process mutex: atomic mkdir, stale when the recorded pid is gone. */
|
|
36
|
+
async function withFileLock(dir, fn) {
|
|
37
|
+
await mkdir(dir, { recursive: true });
|
|
38
|
+
const lock = path.join(dir, ANCHOR_LOCK_DIRNAME);
|
|
39
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
40
|
+
for (;;) {
|
|
41
|
+
try {
|
|
42
|
+
await mkdir(lock);
|
|
43
|
+
await writeFile(path.join(lock, "pid"), String(process.pid));
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
if (err.code !== "EEXIST")
|
|
48
|
+
throw err;
|
|
49
|
+
let holder = 0;
|
|
50
|
+
try {
|
|
51
|
+
holder = Number.parseInt(await readFile(path.join(lock, "pid"), "utf8"), 10);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// holder is between mkdir and pid write
|
|
55
|
+
}
|
|
56
|
+
if (holder > 0 && holder !== process.pid && !pidAlive(holder)) {
|
|
57
|
+
await rm(lock, { recursive: true, force: true });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (Date.now() >= deadline)
|
|
61
|
+
throw new Error("timed out waiting for the shared browser lock");
|
|
62
|
+
await new Promise((r) => setTimeout(r, LOCK_POLL_MS));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return await fn();
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await rm(lock, { recursive: true, force: true }).catch(() => undefined);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const inflight = new Map();
|
|
73
|
+
/**
|
|
74
|
+
* One shared browser per (run, profile). Concurrent callers in this process
|
|
75
|
+
* share one promise; other processes serialize on a lock dir. The anchor and
|
|
76
|
+
* its CDP address are persisted so stages, resume workers and the sweep reuse
|
|
77
|
+
* them; a dead anchor is restarted and the address refreshed.
|
|
78
|
+
*/
|
|
79
|
+
export function ensureRunProfileBrowser(support, input) {
|
|
80
|
+
const key = `${input.runDir}\0${input.profile.key.name}\0${input.humanLogin === true ? "h" : ""}`;
|
|
81
|
+
const existing = inflight.get(key);
|
|
82
|
+
if (existing !== undefined)
|
|
83
|
+
return existing;
|
|
84
|
+
const promise = withFileLock(anchorDir(input.runDir, input.profile.key.name), async () => {
|
|
85
|
+
const stored = await readPersistedAnchor(input.runDir, input.profile.key.name);
|
|
86
|
+
const result = await support.host.ensureProfileBrowser({
|
|
87
|
+
runId: input.runId,
|
|
88
|
+
browser: input.browser,
|
|
89
|
+
profile: input.profile,
|
|
90
|
+
runner: support.runner ?? defaultBrowserRunner,
|
|
91
|
+
...(input.humanLogin === true ? { humanLogin: true } : {}),
|
|
92
|
+
...(stored !== undefined
|
|
93
|
+
? { previous: { cdpAddress: stored.cdpAddress, anchorEnv: stored.anchorEnv } }
|
|
94
|
+
: {}),
|
|
95
|
+
});
|
|
96
|
+
const changed = stored === undefined ||
|
|
97
|
+
stored.cdpAddress !== result.cdpAddress ||
|
|
98
|
+
JSON.stringify(stored.anchorEnv) !== JSON.stringify(result.anchorEnv);
|
|
99
|
+
const persisted = {
|
|
100
|
+
cdpAddress: result.cdpAddress,
|
|
101
|
+
anchorEnv: result.anchorEnv,
|
|
102
|
+
runId: input.runId,
|
|
103
|
+
profile: input.profile.key.name,
|
|
104
|
+
restarts: (stored?.restarts ?? 0) + (stored !== undefined && changed ? 1 : 0),
|
|
105
|
+
};
|
|
106
|
+
if (changed) {
|
|
107
|
+
const dir = anchorDir(input.runDir, input.profile.key.name);
|
|
108
|
+
const file = path.join(dir, BROWSER_ANCHOR_FILENAME);
|
|
109
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
110
|
+
await writeFile(tmp, `${JSON.stringify(persisted, null, 2)}\n`, { mode: 0o600 });
|
|
111
|
+
await rename(tmp, file);
|
|
112
|
+
await writeSessionOwner({
|
|
113
|
+
runId: input.runId,
|
|
114
|
+
stageId: `anchor:${input.profile.key.name}`,
|
|
115
|
+
runDir: input.runDir,
|
|
116
|
+
env: result.anchorEnv,
|
|
117
|
+
anchor: true,
|
|
118
|
+
profile: input.profile.key.name,
|
|
119
|
+
}).catch(() => undefined);
|
|
120
|
+
}
|
|
121
|
+
return { ...result, restarted: stored !== undefined && changed, persisted };
|
|
122
|
+
}).finally(() => {
|
|
123
|
+
inflight.delete(key);
|
|
124
|
+
});
|
|
125
|
+
inflight.set(key, promise);
|
|
126
|
+
return promise;
|
|
127
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Records carry names, ids and hosts only: never paths, URLs with queries, or cookie values. */
|
|
2
|
+
export type AuditRecord = {
|
|
3
|
+
event: "profile_created";
|
|
4
|
+
scope: string;
|
|
5
|
+
profile: string;
|
|
6
|
+
} | {
|
|
7
|
+
event: "profile_deleted";
|
|
8
|
+
scope: string;
|
|
9
|
+
profile: string;
|
|
10
|
+
} | {
|
|
11
|
+
event: "profile_used";
|
|
12
|
+
scope: string;
|
|
13
|
+
profile: string;
|
|
14
|
+
runId: string;
|
|
15
|
+
stageId: string;
|
|
16
|
+
} | {
|
|
17
|
+
event: "navigation_outside_allowlist";
|
|
18
|
+
runId: string;
|
|
19
|
+
stageId: string;
|
|
20
|
+
profile?: string;
|
|
21
|
+
host: string;
|
|
22
|
+
} | {
|
|
23
|
+
event: "allowlist_unverified";
|
|
24
|
+
runId: string;
|
|
25
|
+
stageId: string;
|
|
26
|
+
profile?: string;
|
|
27
|
+
reason: string;
|
|
28
|
+
};
|
|
29
|
+
export interface AuditSink {
|
|
30
|
+
record(entry: AuditRecord): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
export declare function localAuditLogPath(): string;
|
|
33
|
+
export declare function createLocalAuditSink(file?: string): AuditSink;
|
|
34
|
+
export declare function createMemoryAuditSink(): AuditSink & {
|
|
35
|
+
records: AuditRecord[];
|
|
36
|
+
};
|
|
37
|
+
/** Audit must never break a run. */
|
|
38
|
+
export declare function safeAudit(sink: AuditSink | undefined, entry: AuditRecord): Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { appendFile, chmod, mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { globalStageflowHome } from "../project/globalHome.js";
|
|
4
|
+
export function localAuditLogPath() {
|
|
5
|
+
return path.join(globalStageflowHome(), "browser", "audit.jsonl");
|
|
6
|
+
}
|
|
7
|
+
export function createLocalAuditSink(file) {
|
|
8
|
+
return {
|
|
9
|
+
async record(entry) {
|
|
10
|
+
const target = file ?? localAuditLogPath();
|
|
11
|
+
await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
12
|
+
await appendFile(target, `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`, { mode: 0o600 });
|
|
13
|
+
await chmod(target, 0o600).catch(() => undefined);
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function createMemoryAuditSink() {
|
|
18
|
+
const records = [];
|
|
19
|
+
return {
|
|
20
|
+
records,
|
|
21
|
+
async record(entry) {
|
|
22
|
+
records.push(entry);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Audit must never break a run. */
|
|
27
|
+
export async function safeAudit(sink, entry) {
|
|
28
|
+
try {
|
|
29
|
+
await (sink ?? createLocalAuditSink()).record(entry);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// best-effort
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { StageBrowserConfig } from "../types/stage.js";
|
|
2
|
+
import type { ProfileLock } from "./profileLock.js";
|
|
3
|
+
import type { AuditSink } from "./auditSink.js";
|
|
4
|
+
import type { ProfileHandle, ProfileStore } from "./profileStore.js";
|
|
5
|
+
export type BrowserEnv = Record<string, string>;
|
|
6
|
+
export type BrowserStageRequest = {
|
|
7
|
+
runId: string;
|
|
8
|
+
stageId: string;
|
|
9
|
+
browser: StageBrowserConfig;
|
|
10
|
+
profile?: ProfileHandle;
|
|
11
|
+
/** A human must see and use this browser; headless fallback does not apply. */
|
|
12
|
+
humanLogin?: boolean;
|
|
13
|
+
/** Required for a stage with a profile: the shared browser's CDP address. */
|
|
14
|
+
cdpAddress?: string;
|
|
15
|
+
};
|
|
16
|
+
/** The shared browser of one (run, profile): its CDP address and the env that owns it. */
|
|
17
|
+
export type ProfileBrowser = {
|
|
18
|
+
cdpAddress: string;
|
|
19
|
+
anchorEnv: BrowserEnv;
|
|
20
|
+
};
|
|
21
|
+
export type ProfileBrowserRequest = {
|
|
22
|
+
runId: string;
|
|
23
|
+
browser: StageBrowserConfig;
|
|
24
|
+
profile: ProfileHandle;
|
|
25
|
+
humanLogin?: boolean;
|
|
26
|
+
runner: BrowserRunner;
|
|
27
|
+
/** Persisted anchor from an earlier call; reused when still alive, else restarted. */
|
|
28
|
+
previous?: ProfileBrowser;
|
|
29
|
+
};
|
|
30
|
+
export type ProfileBrowserResult = ProfileBrowser & {
|
|
31
|
+
/** True when a previous anchor existed but its address changed or it was replaced. */
|
|
32
|
+
restarted: boolean;
|
|
33
|
+
};
|
|
34
|
+
export interface BrowserHost {
|
|
35
|
+
/**
|
|
36
|
+
* Starts or reuses the one browser for (run, profile) and returns its CDP
|
|
37
|
+
* address. Callers serialize per (run, profile) and persist the result.
|
|
38
|
+
*/
|
|
39
|
+
ensureProfileBrowser(request: ProfileBrowserRequest): Promise<ProfileBrowserResult>;
|
|
40
|
+
/** Env of one stage's agent-browser session; with a profile it attaches to `cdpAddress` in its own tab. */
|
|
41
|
+
stageEnv(request: BrowserStageRequest): Promise<BrowserEnv>;
|
|
42
|
+
/** Env of the session that owns the profile's browser (anchor, or `sf browser login`). */
|
|
43
|
+
profileBrowserEnv(request: Omit<BrowserStageRequest, "stageId" | "cdpAddress"> & {
|
|
44
|
+
profile: ProfileHandle;
|
|
45
|
+
}): Promise<BrowserEnv>;
|
|
46
|
+
}
|
|
47
|
+
/** Runs `agent-browser <args>` with exactly `env` (plus PATH). Injectable for tests. */
|
|
48
|
+
export type BrowserRunner = (args: string[], env: BrowserEnv,
|
|
49
|
+
/** Per-call limit; the default runner allows 20 s. */
|
|
50
|
+
options?: {
|
|
51
|
+
timeoutMs?: number;
|
|
52
|
+
}) => Promise<{
|
|
53
|
+
code: number | null;
|
|
54
|
+
stdout?: string;
|
|
55
|
+
}>;
|
|
56
|
+
export type StageBrowserSupport = {
|
|
57
|
+
host: BrowserHost;
|
|
58
|
+
profiles: ProfileStore;
|
|
59
|
+
runner?: BrowserRunner;
|
|
60
|
+
/** Profile lock; defaults to the local lock-file implementation. */
|
|
61
|
+
locks?: ProfileLock;
|
|
62
|
+
/** Poll interval while a stage waits for a busy profile. */
|
|
63
|
+
lockPollMs?: number;
|
|
64
|
+
/** Upper bound for waiting on the daemon to exit after `close`. */
|
|
65
|
+
closeWaitMs?: number;
|
|
66
|
+
/** Login-check bounds; defaults suit real browsers. */
|
|
67
|
+
loginCheck?: {
|
|
68
|
+
waitMs?: number;
|
|
69
|
+
settleMs?: number;
|
|
70
|
+
};
|
|
71
|
+
/** Root holding per-session socket dirs; defaults to the local host's root. */
|
|
72
|
+
socketRoot?: string;
|
|
73
|
+
/** Defaults to the local audit log in the Stageflow home. */
|
|
74
|
+
audit?: AuditSink;
|
|
75
|
+
/** Host-blocked sites; defaults to `browser.blocked_sites` from Host config. */
|
|
76
|
+
blockedSites?: readonly string[];
|
|
77
|
+
/** Screen detection for human login stages; defaults to the real Host. */
|
|
78
|
+
display?: () => {
|
|
79
|
+
hasDisplay: boolean;
|
|
80
|
+
docker: boolean;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
export declare const BROWSER_ENV_PREFIX = "AGENT_BROWSER_";
|
|
84
|
+
export declare function shortHash(value: string, length: number): string;
|
|
85
|
+
export declare const BROWSER_ENV_FILENAME = "browser-env.json";
|
|
86
|
+
export declare const BROWSER_POLICY_FILENAME = "browser-policy.json";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export const BROWSER_ENV_PREFIX = "AGENT_BROWSER_";
|
|
3
|
+
export function shortHash(value, length) {
|
|
4
|
+
return createHash("sha256").update(value).digest("hex").slice(0, length);
|
|
5
|
+
}
|
|
6
|
+
export const BROWSER_ENV_FILENAME = "browser-env.json";
|
|
7
|
+
export const BROWSER_POLICY_FILENAME = "browser-policy.json";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BrowserRunner } from "./browserHost.js";
|
|
2
|
+
export type BrowserSweepOptions = {
|
|
3
|
+
isRunLive: (runId: string) => Promise<boolean>;
|
|
4
|
+
socketRoot?: string;
|
|
5
|
+
runner?: BrowserRunner;
|
|
6
|
+
closeWaitMs?: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* At Host start: close sessions whose run is gone or terminal, clean leftover
|
|
10
|
+
* files, and leave sessions of live or waiting runs alone.
|
|
11
|
+
*/
|
|
12
|
+
export declare function sweepOrphanBrowserSessions(options: BrowserSweepOptions): Promise<{
|
|
13
|
+
closed: string[];
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { BROWSER_OWNER_FILENAME, closeBrowserSession, } from "./browserTeardown.js";
|
|
4
|
+
import { defaultSocketRoot } from "./localBrowserHost.js";
|
|
5
|
+
import { rmdir } from "node:fs/promises";
|
|
6
|
+
async function readOwner(dir) {
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(await readFile(path.join(dir, BROWSER_OWNER_FILENAME), "utf8"));
|
|
9
|
+
if (typeof parsed.runId === "string" && parsed.env?.AGENT_BROWSER_SESSION) {
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// unreadable owner: treated as ownerless below
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* At Host start: close sessions whose run is gone or terminal, clean leftover
|
|
20
|
+
* files, and leave sessions of live or waiting runs alone.
|
|
21
|
+
*/
|
|
22
|
+
export async function sweepOrphanBrowserSessions(options) {
|
|
23
|
+
const root = options.socketRoot ?? defaultSocketRoot(process.platform);
|
|
24
|
+
const closed = [];
|
|
25
|
+
let dirs;
|
|
26
|
+
try {
|
|
27
|
+
dirs = await readdir(root);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return { closed };
|
|
31
|
+
}
|
|
32
|
+
const owners = [];
|
|
33
|
+
for (const name of dirs) {
|
|
34
|
+
const owner = await readOwner(path.join(root, name));
|
|
35
|
+
if (owner !== undefined)
|
|
36
|
+
owners.push(owner);
|
|
37
|
+
}
|
|
38
|
+
// Stage sessions attach to the anchor, so they go first.
|
|
39
|
+
owners.sort((x, y) => Number(x.anchor === true) - Number(y.anchor === true));
|
|
40
|
+
for (const owner of owners) {
|
|
41
|
+
if (await options.isRunLive(owner.runId).catch(() => true))
|
|
42
|
+
continue;
|
|
43
|
+
const { gone } = await closeBrowserSession(owner.env, {
|
|
44
|
+
...(options.runner !== undefined ? { runner: options.runner } : {}),
|
|
45
|
+
...(options.closeWaitMs !== undefined
|
|
46
|
+
? { closeWaitMs: options.closeWaitMs }
|
|
47
|
+
: {}),
|
|
48
|
+
});
|
|
49
|
+
if (gone)
|
|
50
|
+
closed.push(owner.env.AGENT_BROWSER_SESSION);
|
|
51
|
+
}
|
|
52
|
+
await rmdir(root).catch(() => undefined);
|
|
53
|
+
return { closed };
|
|
54
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { StageLogEvent } from "../runstore/port.js";
|
|
2
|
+
import { type BrowserEnv, type BrowserRunner, type StageBrowserSupport } from "./browserHost.js";
|
|
3
|
+
export declare const BROWSER_OWNER_FILENAME = "owner.json";
|
|
4
|
+
/** Written once a stage's tab and session are closed; cleared when its env is resolved again. */
|
|
5
|
+
export declare const BROWSER_CLOSED_FILENAME = "browser-closed.json";
|
|
6
|
+
export type BrowserSessionOwner = {
|
|
7
|
+
runId: string;
|
|
8
|
+
stageId: string;
|
|
9
|
+
runDir: string;
|
|
10
|
+
env: BrowserEnv;
|
|
11
|
+
/** The run's shared browser for `profile`; stage sessions attach to it. */
|
|
12
|
+
anchor?: true;
|
|
13
|
+
profile?: string;
|
|
14
|
+
};
|
|
15
|
+
export declare const defaultBrowserRunner: BrowserRunner;
|
|
16
|
+
export declare function writeSessionOwner(owner: BrowserSessionOwner): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Gracefully closes the session with the exact env it was launched with, waits
|
|
19
|
+
* (bounded) for the daemon to disappear, then removes the leftover socket-dir
|
|
20
|
+
* files. Never signals Chrome: a hard kill loses cookie persistence.
|
|
21
|
+
*/
|
|
22
|
+
export declare function closeBrowserSession(env: BrowserEnv, options?: {
|
|
23
|
+
runner?: BrowserRunner;
|
|
24
|
+
closeWaitMs?: number;
|
|
25
|
+
}): Promise<{
|
|
26
|
+
gone: boolean;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Closes one stage's tab (when it is attached to a shared browser) and then
|
|
30
|
+
* its session. Chrome, the lease and the other stages stay up. No-op for
|
|
31
|
+
* stages that never resolved a browser env.
|
|
32
|
+
*/
|
|
33
|
+
export declare function teardownStageBrowser(support: StageBrowserSupport, input: StageTeardownInput): Promise<void>;
|
|
34
|
+
type StageEvents = () => Promise<StageLogEvent[]>;
|
|
35
|
+
type StageTeardownInput = {
|
|
36
|
+
runId: string;
|
|
37
|
+
runDir: string;
|
|
38
|
+
stageId: string;
|
|
39
|
+
/** Reads the stage's persisted tool activity for the soft allowlist audit. */
|
|
40
|
+
events?: StageEvents;
|
|
41
|
+
};
|
|
42
|
+
/** Gracefully closes every anchor browser the run started. */
|
|
43
|
+
export declare function closeRunAnchors(support: StageBrowserSupport, run: {
|
|
44
|
+
runDir: string;
|
|
45
|
+
}): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Run end, cancel, abandon or failure: closes stage sessions and tabs, then
|
|
48
|
+
* the run's anchors, then releases the run's profile leases. `only` closes
|
|
49
|
+
* just those stages and leaves anchor and leases alone.
|
|
50
|
+
*/
|
|
51
|
+
export declare function teardownRunBrowsers(support: StageBrowserSupport, run: {
|
|
52
|
+
runId: string;
|
|
53
|
+
runDir: string;
|
|
54
|
+
events?: (stageId: string) => Promise<StageLogEvent[]>;
|
|
55
|
+
}, only?: (stageId: string) => boolean): Promise<void>;
|
|
56
|
+
export {};
|