libretto 0.6.28 → 0.6.30-experimental-runtime-libretto.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.
@@ -6,6 +6,7 @@ import {
6
6
  connect,
7
7
  disconnectBrowser,
8
8
  runClose,
9
+ resolveWindowPosition,
9
10
  resolveViewport
10
11
  } from "../core/browser.js";
11
12
  import { parseViewportArg } from "./browser.js";
@@ -20,7 +21,9 @@ import {
20
21
  writeSessionState
21
22
  } from "../core/session.js";
22
23
  import { warnIfLibrettoVersionsDiffer } from "../core/skill-version.js";
23
- import { readLibrettoConfig } from "../core/config.js";
24
+ import {
25
+ readLibrettoConfig
26
+ } from "../core/config.js";
24
27
  import { renderSnapshotDiff } from "../../shared/snapshot/diff-snapshots.js";
25
28
  import {
26
29
  getProviderStartupTimeoutMs,
@@ -51,6 +54,20 @@ import {
51
54
  withRequiredSession
52
55
  } from "./shared.js";
53
56
  const require2 = moduleBuiltin.createRequire(import.meta.url);
57
+ function createRunBrowserConfig(args) {
58
+ if (args.providerName) {
59
+ return {
60
+ kind: "provider",
61
+ providerName: args.providerName
62
+ };
63
+ }
64
+ return {
65
+ kind: "launch",
66
+ headed: !args.headless,
67
+ viewport: args.viewport ?? { width: 1366, height: 768 },
68
+ ...!args.headless && args.windowPosition ? { windowPosition: args.windowPosition } : {}
69
+ };
70
+ }
54
71
  function writeDaemonExecOutput(output) {
55
72
  if (output?.stdout) {
56
73
  process.stdout.write(output.stdout);
@@ -427,14 +444,7 @@ async function runIntegrationFromFile(args, logger) {
427
444
  config: {
428
445
  session: args.session,
429
446
  experiments: args.experiments,
430
- browser: args.providerName ? {
431
- kind: "provider",
432
- providerName: args.providerName
433
- } : {
434
- kind: "launch",
435
- headed: !args.headless,
436
- viewport: args.viewport ?? { width: 1366, height: 768 }
437
- },
447
+ browser: createRunBrowserConfig(args),
438
448
  workflow: {
439
449
  integrationPath: absoluteIntegrationPath,
440
450
  params: args.params,
@@ -690,15 +700,18 @@ const runCommand = SimpleCLI.command({
690
700
  });
691
701
  console.log(`Connecting to ${providerName} browser...`);
692
702
  }
703
+ const headless = daemonProviderName ? true : headlessMode ?? false;
704
+ const windowPosition = headless ? void 0 : resolveWindowPosition(ctx.logger);
693
705
  await runIntegrationFromFile(
694
706
  {
695
707
  integrationPath: input.integrationFile,
696
708
  session: ctx.session,
697
709
  params,
698
710
  tsconfigPath: input.tsconfig,
699
- headless: daemonProviderName ? true : headlessMode ?? false,
711
+ headless,
700
712
  visualize,
701
713
  viewport,
714
+ windowPosition,
702
715
  accessMode: input.readOnly ? "read-only" : input.writeAccess ? "write-access" : readLibrettoConfig().sessionMode ?? "write-access",
703
716
  providerName: daemonProviderName,
704
717
  stayOpenOnSuccess: input.stayOpenOnSuccess,
@@ -725,6 +738,7 @@ const executionCommands = {
725
738
  resume: resumeCommand
726
739
  };
727
740
  export {
741
+ createRunBrowserConfig,
728
742
  execCommand,
729
743
  execInput,
730
744
  executionCommands,
@@ -1027,6 +1027,7 @@ export {
1027
1027
  normalizeUrl,
1028
1028
  resolvePath,
1029
1029
  resolveViewport,
1030
+ resolveWindowPosition,
1030
1031
  runClose,
1031
1032
  runCloseAll,
1032
1033
  runConnect,
@@ -49,6 +49,7 @@ import {
49
49
  import { WorkflowController } from "../workflow-runner/runner.js";
50
50
  import { validateWorkflowInput } from "../../../shared/workflow/workflow.js";
51
51
  import { captureAuthProfileStorageState } from "../../../shared/workflow/auth-profile-state.js";
52
+ import { applyWindowPosition } from "../../../shared/run/window-position.js";
52
53
  function isOperationalPage(page) {
53
54
  const url = page.url();
54
55
  return !url.startsWith("devtools://") && !url.startsWith("chrome-error://");
@@ -266,6 +267,7 @@ class BrowserDaemon {
266
267
  userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
267
268
  });
268
269
  const page = await context.newPage();
270
+ await applyWindowPosition(browser, context, page, config.windowPosition);
269
271
  page.setDefaultTimeout(3e4);
270
272
  page.setDefaultNavigationTimeout(45e3);
271
273
  const daemon = await BrowserDaemon.initialize({
@@ -22,7 +22,6 @@ import {
22
22
  } from "../../shared/workflow/workflow.js";
23
23
  import { normalizeCredentialNames } from "../../shared/workflow/credentials.js";
24
24
  const DEFAULT_RUNTIME_EXTERNALS = [
25
- "libretto",
26
25
  "playwright",
27
26
  "playwright-core",
28
27
  "chromium-bidi"
@@ -408,14 +407,58 @@ function resolveDependencyVersion(sourceDir, packageName, fallbackVersion) {
408
407
  `Unable to determine a version for external package "${packageName}". Add it to your package.json or remove it from --external.`
409
408
  );
410
409
  }
410
+ function resolveLibrettoDependencyVersion(sourceDir, packageName) {
411
+ try {
412
+ const librettoManifestPath = require2.resolve("libretto/package.json", {
413
+ paths: [sourceDir]
414
+ });
415
+ const version2 = readDependencyVersionFromManifest(
416
+ readPackageManifest(librettoManifestPath),
417
+ packageName
418
+ );
419
+ if (version2) {
420
+ return version2;
421
+ }
422
+ } catch {
423
+ }
424
+ const version = readDependencyVersionFromManifest(
425
+ readPackageManifest(join(CURRENT_LIBRETTO_PACKAGE_DIR, "package.json")),
426
+ packageName
427
+ );
428
+ if (version) {
429
+ return version;
430
+ }
431
+ try {
432
+ const manifestPath = require2.resolve(`${packageName}/package.json`, {
433
+ paths: [CURRENT_LIBRETTO_PACKAGE_DIR]
434
+ });
435
+ return readPackageManifest(manifestPath).version ?? null;
436
+ } catch {
437
+ return null;
438
+ }
439
+ }
411
440
  function writeDeployManifest(args) {
441
+ const dependencyPackages = [
442
+ .../* @__PURE__ */ new Set([
443
+ ...BUILT_IN_MANIFEST_DEPENDENCIES,
444
+ ...args.runtimeExternals.filter(
445
+ (packageName) => resolveLibrettoDependencyVersion(args.sourceDir, packageName)
446
+ ),
447
+ ...args.additionalExternals
448
+ ])
449
+ ];
412
450
  const dependencies = Object.fromEntries(
413
- [...BUILT_IN_MANIFEST_DEPENDENCIES, ...args.additionalExternals].map(
414
- (packageName) => [
451
+ dependencyPackages.map((packageName) => {
452
+ const fallbackVersion = packageName === "libretto" ? args.librettoDependency : resolveLibrettoDependencyVersion(args.sourceDir, packageName) ?? void 0;
453
+ return [
415
454
  packageName,
416
- packageName === "libretto" ? args.librettoDependency : resolveDependencyVersion(args.sourceDir, packageName)
417
- ]
418
- )
455
+ packageName === "libretto" ? args.librettoDependency : resolveDependencyVersion(
456
+ args.sourceDir,
457
+ packageName,
458
+ fallbackVersion
459
+ )
460
+ ];
461
+ })
419
462
  );
420
463
  writeFileSync(
421
464
  join(args.outputDir, "package.json"),
@@ -649,7 +692,7 @@ function createBootstrapSource(args) {
649
692
  authProfileRefresh: workflow.authProfileRefresh
650
693
  })});`
651
694
  ).join("\n");
652
- return `import { createRequire } from "node:module";
695
+ return `import { createRequire, Module } from "node:module";
653
696
  import { existsSync, writeFileSync } from "node:fs";
654
697
  import { tmpdir } from "node:os";
655
698
  import { join } from "node:path";
@@ -665,6 +708,45 @@ const BUNDLE_FILENAME = join(
665
708
  const nativeRequire = createRequire(
666
709
  join(tmpdir(), ${JSON.stringify("libretto-deploy-bootstrap.cjs")}),
667
710
  );
711
+ const packageRequire = createRequire(import.meta.url || __filename);
712
+
713
+ function isBarePackageSpecifier(specifier) {
714
+ return (
715
+ !specifier.startsWith(".") &&
716
+ !specifier.startsWith("/") &&
717
+ !specifier.startsWith("node:") &&
718
+ !/^[A-Za-z]:[\\\\/]/.test(specifier)
719
+ );
720
+ }
721
+
722
+ function loadImplementation() {
723
+ const originalRequire = Module.prototype.require;
724
+ function patchedRequire(specifier) {
725
+ try {
726
+ return originalRequire.call(this, specifier);
727
+ } catch (error) {
728
+ if (
729
+ error?.code === "MODULE_NOT_FOUND" &&
730
+ isBarePackageSpecifier(specifier)
731
+ ) {
732
+ Module.prototype.require = originalRequire;
733
+ try {
734
+ return packageRequire(specifier);
735
+ } finally {
736
+ Module.prototype.require = patchedRequire;
737
+ }
738
+ }
739
+ throw error;
740
+ }
741
+ }
742
+ Module.prototype.require = patchedRequire;
743
+
744
+ try {
745
+ return nativeRequire(ensureBundleFile());
746
+ } finally {
747
+ Module.prototype.require = originalRequire;
748
+ }
749
+ }
668
750
 
669
751
  function ensureBundleFile() {
670
752
  if (!existsSync(BUNDLE_FILENAME)) {
@@ -679,7 +761,7 @@ function ensureBundleFile() {
679
761
 
680
762
  function createWorkflowProxy(workflowName, metadata) {
681
763
  const handler = async (ctx, input) => {
682
- const impl = nativeRequire(ensureBundleFile());
764
+ const impl = loadImplementation();
683
765
  const target = getWorkflowFromModuleExports(impl, workflowName);
684
766
  if (!target || typeof target.run !== "function") {
685
767
  throw new Error(
@@ -716,6 +798,11 @@ ${exportLines}
716
798
  async function writeBundledDeployEntrypoint(args) {
717
799
  try {
718
800
  const unresolvedWorkspaceImports = /* @__PURE__ */ new Map();
801
+ const discoveryExternalPackages = /* @__PURE__ */ new Set([
802
+ ...args.externalPackages,
803
+ "libretto"
804
+ ]);
805
+ const unresolvedDiscoveryWorkspaceImports = /* @__PURE__ */ new Map();
719
806
  const implementationBuild = await build({
720
807
  absWorkingDir: args.absSourceDir,
721
808
  bundle: true,
@@ -736,6 +823,25 @@ async function writeBundledDeployEntrypoint(args) {
736
823
  target: "node20",
737
824
  write: false
738
825
  });
826
+ const discoveryBuild = await build({
827
+ absWorkingDir: args.absSourceDir,
828
+ bundle: true,
829
+ entryPoints: [args.absEntryPoint],
830
+ external: [...discoveryExternalPackages],
831
+ format: "cjs",
832
+ outfile: "prebundled-discovery.cjs",
833
+ platform: "node",
834
+ plugins: [
835
+ workspaceSourcePlugin(
836
+ args.workspacePackages,
837
+ discoveryExternalPackages,
838
+ unresolvedDiscoveryWorkspaceImports
839
+ )
840
+ ],
841
+ splitting: false,
842
+ target: "node20",
843
+ write: false
844
+ });
739
845
  const [unresolvedImport] = unresolvedWorkspaceImports;
740
846
  if (unresolvedImport) {
741
847
  const [importPath, packageDir] = unresolvedImport;
@@ -743,6 +849,13 @@ async function writeBundledDeployEntrypoint(args) {
743
849
  `Unable to resolve workspace import "${importPath}" from ${packageDir}.`
744
850
  );
745
851
  }
852
+ const [unresolvedDiscoveryImport] = unresolvedDiscoveryWorkspaceImports;
853
+ if (unresolvedDiscoveryImport) {
854
+ const [importPath, packageDir] = unresolvedDiscoveryImport;
855
+ throw new Error(
856
+ `Unable to resolve workspace import "${importPath}" from ${packageDir}.`
857
+ );
858
+ }
746
859
  const bundledImplementation = implementationBuild.outputFiles?.find(
747
860
  (file) => file.path.endsWith("prebundled.cjs")
748
861
  );
@@ -751,11 +864,17 @@ async function writeBundledDeployEntrypoint(args) {
751
864
  "Bundler did not produce a deployment implementation file."
752
865
  );
753
866
  }
867
+ const bundledDiscovery = discoveryBuild.outputFiles?.find(
868
+ (file) => file.path.endsWith("prebundled-discovery.cjs")
869
+ );
870
+ if (!bundledDiscovery) {
871
+ throw new Error("Bundler did not produce a deployment discovery file.");
872
+ }
754
873
  const workflows = discoverBundledWorkflows({
755
874
  absEntryPoint: args.absEntryPoint,
756
875
  absSourceDir: args.absSourceDir,
757
- bundleBuffer: Buffer.from(bundledImplementation.contents),
758
- externalPackages: args.externalPackages
876
+ bundleBuffer: Buffer.from(bundledDiscovery.contents),
877
+ externalPackages: discoveryExternalPackages
759
878
  });
760
879
  writeFileSync(
761
880
  join(args.outputDir, "index.js"),
@@ -825,6 +944,7 @@ async function createHostedDeployPackage(args) {
825
944
  deploymentName: args.deploymentName,
826
945
  librettoDependency,
827
946
  outputDir,
947
+ runtimeExternals: [...DEFAULT_RUNTIME_EXTERNALS],
828
948
  sourceDir: absSourceDir
829
949
  });
830
950
  writeDeployMetadata({ outputDir, workflows: workflowsWithShareableSource });
@@ -9,6 +9,9 @@ import {
9
9
  SessionStateFileSchema
10
10
  } from "../state/session-state.js";
11
11
  import { readLibrettoConfig } from "../../cli/core/config.js";
12
+ import {
13
+ applyWindowPosition
14
+ } from "./window-position.js";
12
15
  async function pickFreePort() {
13
16
  return await new Promise((resolve, reject) => {
14
17
  const server = createServer();
@@ -27,38 +30,6 @@ async function pickFreePort() {
27
30
  function resolveWindowPosition() {
28
31
  return readLibrettoConfig().windowPosition;
29
32
  }
30
- async function applyWindowPosition(browser, context, page, windowPosition) {
31
- if (!windowPosition) {
32
- return;
33
- }
34
- const requestedBounds = {
35
- left: windowPosition.x,
36
- top: windowPosition.y,
37
- windowState: "normal"
38
- };
39
- const pageCdp = await context.newCDPSession(page);
40
- let browserCdp;
41
- try {
42
- const targetInfo = await pageCdp.send("Target.getTargetInfo");
43
- const targetId = targetInfo.targetInfo?.targetId;
44
- browserCdp = await browser.newBrowserCDPSession();
45
- const windowResult = await browserCdp.send(
46
- "Browser.getWindowForTarget",
47
- targetId ? { targetId } : {}
48
- );
49
- await browserCdp.send("Browser.setWindowBounds", {
50
- windowId: windowResult.windowId,
51
- bounds: requestedBounds
52
- });
53
- await new Promise((resolve) => setTimeout(resolve, 250));
54
- } catch {
55
- } finally {
56
- await pageCdp.detach().catch(() => {
57
- });
58
- await browserCdp?.detach().catch(() => {
59
- });
60
- }
61
- }
62
33
  async function launchBrowser({
63
34
  sessionName,
64
35
  headless = false,
@@ -0,0 +1,9 @@
1
+ import { Browser, BrowserContext, Page } from 'playwright';
2
+
3
+ type WindowPosition = {
4
+ x: number;
5
+ y: number;
6
+ };
7
+ declare function applyWindowPosition(browser: Browser, context: BrowserContext, page: Page, windowPosition: WindowPosition | undefined): Promise<void>;
8
+
9
+ export { type WindowPosition, applyWindowPosition };
@@ -0,0 +1,36 @@
1
+ async function applyWindowPosition(browser, context, page, windowPosition) {
2
+ if (!windowPosition) {
3
+ return;
4
+ }
5
+ const requestedBounds = {
6
+ left: windowPosition.x,
7
+ top: windowPosition.y,
8
+ windowState: "normal"
9
+ };
10
+ let pageCdp;
11
+ let browserCdp;
12
+ try {
13
+ pageCdp = await context.newCDPSession(page);
14
+ const targetInfo = await pageCdp.send("Target.getTargetInfo");
15
+ const targetId = targetInfo.targetInfo?.targetId;
16
+ browserCdp = await browser.newBrowserCDPSession();
17
+ const windowResult = await browserCdp.send(
18
+ "Browser.getWindowForTarget",
19
+ targetId ? { targetId } : {}
20
+ );
21
+ await browserCdp.send("Browser.setWindowBounds", {
22
+ windowId: windowResult.windowId,
23
+ bounds: requestedBounds
24
+ });
25
+ await new Promise((resolve) => setTimeout(resolve, 250));
26
+ } catch {
27
+ } finally {
28
+ await pageCdp?.detach().catch(() => {
29
+ });
30
+ await browserCdp?.detach().catch(() => {
31
+ });
32
+ }
33
+ }
34
+ export {
35
+ applyWindowPosition
36
+ };
@@ -5,8 +5,8 @@ import '../../runtime/recovery/page-fallbacks.js';
5
5
  import 'ai';
6
6
 
7
7
  type LibrettoAuthenticateOptions = {
8
- validate: (ctx: LibrettoWorkflowContext) => Promise<boolean> | boolean;
9
- fallback: (ctx: LibrettoWorkflowContext, credentials: Record<string, string>) => Promise<void> | void;
8
+ isSignedIn: (ctx: LibrettoWorkflowContext) => Promise<boolean> | boolean;
9
+ signIn: (ctx: LibrettoWorkflowContext, credentials: Record<string, string>) => Promise<void> | void;
10
10
  credentials?: Record<string, unknown>;
11
11
  envPrefix?: string;
12
12
  };
@@ -1,13 +1,13 @@
1
1
  async function librettoAuthenticate(ctx, options) {
2
- if (await options.validate(ctx)) {
2
+ if (await options.isSignedIn(ctx)) {
3
3
  return { usedProfile: true };
4
4
  }
5
5
  const credentials = normalizeCredentials(
6
6
  options.credentials ?? readCredentialsFromEnv(options.envPrefix)
7
7
  );
8
- await options.fallback(ctx, credentials);
9
- if (!await options.validate(ctx)) {
10
- throw new Error("Authentication fallback completed, but validation still failed.");
8
+ await options.signIn(ctx, credentials);
9
+ if (!await options.isSignedIn(ctx)) {
10
+ throw new Error("Sign-in completed, but the session is still not signed in.");
11
11
  }
12
12
  return { usedProfile: false };
13
13
  }
package/docs/releasing.md CHANGED
@@ -66,7 +66,7 @@ The root `scripts/prepare-release.sh` script does the following:
66
66
  1. Checks that the working tree is clean.
67
67
  2. Updates local `main` from `origin/main`.
68
68
  3. Runs `pnpm install --frozen-lockfile`, `pnpm --filter libretto type-check`, and `pnpm --filter libretto test`.
69
- 4. Checks whether `packages/affordance` changed since the current Libretto release tag. If it changed, the script runs Affordance type-check/tests and bumps `packages/affordance/package.json` by one patch version when the current Affordance version is already published to npm.
69
+ 4. Checks whether `packages/affordance` changed since the current Libretto release tag, excluding `packages/affordance/package.json` version-only changes. If source/package contents changed, the script runs Affordance type-check/tests and bumps `packages/affordance/package.json` by one patch version when the current Affordance version is already published to npm.
70
70
  5. Bumps the version in `packages/libretto/package.json`.
71
71
  6. Creates a release branch.
72
72
  7. Commits the version bump.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libretto",
3
- "version": "0.6.28",
3
+ "version": "0.6.30-experimental-runtime-libretto.0",
4
4
  "description": "AI-powered browser automation library and CLI built on Playwright",
5
5
  "license": "MIT",
6
6
  "homepage": "https://libretto.sh",
@@ -33,6 +33,21 @@
33
33
  "default": "./dist/index.js"
34
34
  }
35
35
  },
36
+ "scripts": {
37
+ "sync:mirrors": "node ../dev-tools/scripts/sync-mirrors.mjs",
38
+ "check:mirrors": "node ../dev-tools/scripts/check-mirrors-sync.mjs",
39
+ "sync-skills": "pnpm run sync:mirrors",
40
+ "check:skills": "pnpm run check:mirrors",
41
+ "build": "tsup --config tsup.config.ts",
42
+ "lint": "lintcn lint --tsconfig tsconfig.json",
43
+ "type-check": "tsc --noEmit",
44
+ "test": "turbo run test:vitest --filter=libretto --log-order=grouped",
45
+ "test:vitest": "vitest run",
46
+ "test:watch": "vitest",
47
+ "cli": "node dist/index.js",
48
+ "generate-changelog": "tsx scripts/generate-changelog.ts",
49
+ "prepack": "pnpm run build"
50
+ },
36
51
  "peerDependencies": {
37
52
  "@ai-sdk/google": "^3.0.51",
38
53
  "@ai-sdk/google-vertex": "^4.0.80"
@@ -65,25 +80,11 @@
65
80
  "dependencies": {
66
81
  "@ai-sdk/anthropic": "^3.0.66",
67
82
  "@ai-sdk/openai": "^3.0.66",
83
+ "affordance": "workspace:^",
68
84
  "ai": "^6.0.116",
69
85
  "esbuild": "^0.27.0",
70
86
  "playwright": "^1.58.2",
71
87
  "tsx": "^4.21.0",
72
- "zod": "^4.3.6",
73
- "affordance": "^0.2.1"
74
- },
75
- "scripts": {
76
- "sync:mirrors": "node ../dev-tools/scripts/sync-mirrors.mjs",
77
- "check:mirrors": "node ../dev-tools/scripts/check-mirrors-sync.mjs",
78
- "sync-skills": "pnpm run sync:mirrors",
79
- "check:skills": "pnpm run check:mirrors",
80
- "build": "tsup --config tsup.config.ts",
81
- "lint": "lintcn lint --tsconfig tsconfig.json",
82
- "type-check": "tsc --noEmit",
83
- "test": "turbo run test:vitest --filter=libretto --log-order=grouped",
84
- "test:vitest": "vitest run",
85
- "test:watch": "vitest",
86
- "cli": "node dist/index.js",
87
- "generate-changelog": "tsx scripts/generate-changelog.ts"
88
+ "zod": "^4.3.6"
88
89
  }
89
- }
90
+ }
@@ -4,7 +4,7 @@ description: "Browser automation CLI for building, maintaining, and running brow
4
4
  license: MIT
5
5
  metadata:
6
6
  author: saffron-health
7
- version: "0.6.28"
7
+ version: "0.6.29"
8
8
  ---
9
9
 
10
10
  ## How Libretto Works
@@ -51,7 +51,11 @@ Prefer to enter sites at a user-facing URL (homepage, login, etc.) on the first
51
51
  - Do not treat visibility as interactivity. If an element will not act, inspect blockers before retrying.
52
52
  - Defer repo/code review until you begin generating code, unless the user explicitly asks for it earlier.
53
53
  - Read and follow guidelines in `references/code-generation-rules.md` before generating or editing production workflow code.
54
- - Validation requires a successful clean `run` with confirmation of the actual returned output, not just process success. Use the same headed or headless mode that the workflow run is already using.
54
+ - For authenticated workflows, manual login is discovery only. After the user logs in, read only the sign-in action logs and identify the required credentials; if credentials are unclear, ask before writing code.
55
+ - Add missing blank `LIBRETTO_CLOUD_<secret_name>=` entries without overwriting populated values. If any required credential is blank, stop and ask the user to fill it; until then, do not inspect logged-in pages, read authenticated network bodies, write workflow code, open validation sessions, or continue discovery.
56
+ - Authenticated workflows must implement `librettoAuthenticate` with declared credentials before validation. Use a reusable `*_totp_secret` credential for authenticator-app MFA, not a one-time `otp_code`; text and email verification codes are not supported for fully automated sign-in.
57
+ - Read `references/website-authentication.md` when you need `librettoAuthenticate` examples or auth-profile details.
58
+ - Validation requires a successful clean `run` on a fresh, unauthenticated session with confirmation of the actual returned output, not just process success. Use the same headed or headless mode that the workflow run is already using.
55
59
  - After validation, always show the user: (1) the output/results from the validation run, and (2) the same command so they can re-run it themselves. Include any `--params`, `--headed`, or `--headless` flags the workflow needs.
56
60
  - Treat exploration sessions as disposable unless the user explicitly wants one kept open.
57
61
  - Get explicit user confirmation before mutating actions or replaying network requests that may have side effects.
@@ -232,13 +236,15 @@ Key fields: `id` (incrementing request id), `ts` (ISO timestamp), `pageId` (page
232
236
  [Context: The user wants to build a new browser workflow and does not yet know the page structure]
233
237
  Assistant: I'll inspect the real site first if needed, but before I finish I'll create `target-workflow.ts` so the task produces reusable automation code.
234
238
  Assistant: [Runs `npx libretto open https://target.example.com --headed`]
239
+ Assistant: [If sign-in is required, follows the authenticated workflow rules before continuing]
235
240
  Assistant: [Reads `references/site-security-review.md` before choosing between passive network inspection, direct browser fetch calls, and Playwright-first automation]
236
241
  Assistant: [Runs `npx libretto snapshot --session <session>`]
237
242
  Assistant: [Uses `snapshot` and `exec` as needed to understand the site and decide the implementation path]
238
243
  Assistant: [Reads `references/code-generation-rules.md` before writing production workflow code]
239
244
  Assistant: I found the working path. I'll now create the workflow file and verify it.
240
245
  Assistant: [Creates or edits `target-workflow.ts` following `references/code-generation-rules.md`]
241
- Assistant: [Runs `npx libretto run ./target-workflow.ts --params '{"status":"open"}'` to validate]
246
+ Assistant: [Runs `npx libretto run ./target-workflow.ts --params '{"status":"open"}'` on a fresh, unauthenticated session to verify both sign-in and workflow behavior]
247
+ Assistant: [If sign-in validation passes and a reusable session is useful, adds an auth profile and reruns validation]
242
248
  Assistant: Validation passed. Here are the results:
243
249
  [Shows the output/results from the validation run]
244
250
  To run it again, use: npx libretto run ./target-workflow.ts --params '{"status":"open"}'
@@ -270,7 +276,7 @@ To run it again, use: npx libretto run ./integration.ts
270
276
  - Read `references/configuration-file-reference.md` when you need to inspect or change `.libretto/config.json` for viewport or session defaults.
271
277
  - Read `references/site-security-review.md` before reviewing the site's security posture and deciding whether to lead with network requests, passive interception, or Playwright DOM automation on a new site.
272
278
  - Read `references/code-generation-rules.md` before writing or editing production workflow files.
273
- - Read `references/auth-profiles.md` when auth-profile behavior is relevant.
279
+ - Read `references/website-authentication.md` when website sign-in implementation or auth-profile behavior is relevant.
274
280
  - Read `references/pages-and-page-targeting.md` when a session has multiple open pages or you need `--page`.
275
281
  - Read `references/action-logs.md` for full action log field descriptions and user-vs-agent event semantics.
276
282
  - If the workflow code is deployed to the Libretto Cloud platform and you need to reference its API docs, fetch [https://libretto.sh/docs/llms.txt](https://libretto.sh/docs/llms.txt) and follow the relevant page links.
@@ -49,7 +49,7 @@ Key points:
49
49
  - After validation is complete and the workflow is confirmed working end to end, remove all `pause()` calls and pause-only workflow params unless the user explicitly says to keep them.
50
50
  - The browser is launched and closed automatically by the CLI. Do not launch or close it in the handler.
51
51
 
52
- ## Workflow Credentials And Auth Profiles
52
+ ## Workflow Credentials And Authentication
53
53
 
54
54
  Declare `credentials` for runtime secrets instead of putting them in the Zod input schema or `--params`. Only declared names are injected into `input.credentials`; local runs read matching `LIBRETTO_CLOUD_` variables from `.env`, and hosted runs read matching Libretto Cloud credentials. For example, `LIBRETTO_CLOUD_OPENAI_API_KEY` becomes `input.credentials.openai_api_key`.
55
55
 
@@ -70,14 +70,7 @@ export default workflow("sentimentWorkflow", {
70
70
  });
71
71
  ```
72
72
 
73
- For logged-in website workflows, unless the user says otherwise, generate both:
74
-
75
- - `authProfile: { name, refresh: true }`
76
- - `librettoAuthenticate(...)` fallback login logic using declared credentials such as `credentials: ["username", "password"]`
77
-
78
- After generating or validating a credentialed workflow, include the local `.env` variable names the user must set, and never print secret values.
79
-
80
- The fallback should validate the signed-in state, log in when validation fails, and validate again before continuing. Follow `references/auth-profiles.md` for profile naming, refresh behavior, hosted profile behavior, and the fallback-login pattern.
73
+ For website workflows that require signing in, build working sign-in logic with `librettoAuthenticate` and verify it from a clean signed-out browser before adding an auth profile. Follow `references/website-authentication.md`.
81
74
 
82
75
  ## Workflow Error Handling
83
76
 
@@ -43,7 +43,7 @@ General guidance: determine whether the site has bot protection and roughly how
43
43
 
44
44
  ### Probe 2: Fetch and XHR Interception
45
45
 
46
- Check whether the site has monkey-patched `window.fetch` or `XMLHttpRequest`. If it has, making your own fetch calls from `page.evaluate()` is risky because the site can inspect call stacks and detect calls that do not originate from its own code.
46
+ Check whether the site has monkey-patched `window.fetch` or `XMLHttpRequest`. Patching is a caution signal, not an automatic blocker. Browser-context network requests are usually fine when the target endpoint is already called by the site with fetch/XHR and there is no strong bot-protection evidence.
47
47
 
48
48
  ```js
49
49
  window.fetch.toString()
@@ -52,7 +52,7 @@ Object.getOwnPropertyDescriptor(window, 'fetch')
52
52
  window.fetch.hasOwnProperty('prototype')
53
53
  ```
54
54
 
55
- Important: some sites use `Proxy` to wrap fetch, which makes `toString()` still return `"[native code]"`. The prototype check is a heuristic, not definitive. If you see any sign of fetch interception, treat it as patched.
55
+ Important: ordinary app instrumentation and Libretto's own page-stability tracking can also wrap fetch/XHR. Treat browser fetch as risky only when the wrapper appears security-related, obfuscated, tied to bot-protection scripts, or likely to inspect call stacks. Some `Proxy` wrappers still stringify as `"[native code]"`, so these checks are only heuristics.
56
56
 
57
57
  ## Choosing a Data Capture Strategy
58
58
 
@@ -66,7 +66,7 @@ When to prioritize this:
66
66
 
67
67
  - The target endpoint is normally called by the site with fetch/XHR
68
68
  - No enterprise bot protection is detected
69
- - `fetch` is not monkey-patched
69
+ - No security-related fetch/XHR interception is detected, or the observed wrapper appears to be ordinary app instrumentation
70
70
  - The API responses are parseable and useful
71
71
  - You need data that requires many API calls (deep pagination, bulk queries) where driving the UI would be slow
72
72
 
@@ -83,7 +83,7 @@ Listen to network responses that the browser naturally makes as you navigate. Yo
83
83
  When to prioritize this:
84
84
 
85
85
  - Enterprise bot protection is detected
86
- - `fetch` is monkey-patched
86
+ - Security-related fetch/XHR interception is detected
87
87
  - The site's normal UI flow triggers API calls that return the data you need
88
88
  - You want to minimize detection risk as much as possible
89
89
 
@@ -112,8 +112,9 @@ Trade-off: it is slower, more fragile against DOM changes, and you only get data
112
112
 
113
113
  | Site Profile | Primary Strategy | Supplement With |
114
114
  | --- | --- | --- |
115
- | No bot protection, fetch/XHR endpoint, fetch not patched | A (`page.evaluate(fetch)`) | Playwright for navigation/auth |
116
- | No bot protection, fetch is patched or endpoint is not fetch/XHR | B (`page.on('response', ...)`) | Playwright for navigation; DOM extraction as fallback |
115
+ | No bot protection, fetch/XHR endpoint, no security-related interception | A (`page.evaluate(fetch)`) | Playwright for navigation/auth |
116
+ | No bot protection, harmless app instrumentation, fetch/XHR endpoint | A (`page.evaluate(fetch)`) | B (`page.on('response', ...)`) if requests fail |
117
+ | No bot protection, security-related interception or endpoint is not fetch/XHR | B (`page.on('response', ...)`) | Playwright for navigation; DOM extraction as fallback |
117
118
  | Bot protection detected | B (`page.on('response', ...)`) | Playwright for navigation; cautious use of `page.evaluate(fetch)` only if needed |
118
119
  | Server-rendered content (no API calls) | C (DOM extraction) | Playwright for all interaction |
119
120