Module: Browserctl
- Defined in:
- lib/browserctl/flows/stdlib/cloudflare_solve.rb,
lib/browserctl.rb,
lib/browserctl/flow.rb,
lib/browserctl/state.rb,
lib/browserctl/client.rb,
lib/browserctl/errors.rb,
lib/browserctl/logger.rb,
lib/browserctl/policy.rb,
lib/browserctl/runner.rb,
lib/browserctl/server.rb,
lib/browserctl/tracing.rb,
lib/browserctl/version.rb,
lib/browserctl/redactor.rb,
lib/browserctl/workflow.rb,
lib/browserctl/constants.rb,
lib/browserctl/detectors.rb,
lib/browserctl/recording.rb,
lib/browserctl/driver/cdp.rb,
lib/browserctl/migrations.rb,
lib/browserctl/error/codes.rb,
lib/browserctl/commands/ask.rb,
lib/browserctl/crash_report.rb,
lib/browserctl/snapshot/ref.rb,
lib/browserctl/state/bundle.rb,
lib/browserctl/commands/data.rb,
lib/browserctl/commands/fill.rb,
lib/browserctl/commands/flow.rb,
lib/browserctl/commands/init.rb,
lib/browserctl/commands/page.rb,
lib/browserctl/flow_registry.rb,
lib/browserctl/state/mutator.rb,
lib/browserctl/commands/click.rb,
lib/browserctl/commands/state.rb,
lib/browserctl/commands/trace.rb,
lib/browserctl/format_version.rb,
lib/browserctl/orphan_sweeper.rb,
lib/browserctl/replay/context.rb,
lib/browserctl/trace/renderer.rb,
lib/browserctl/commands/cookie.rb,
lib/browserctl/commands/daemon.rb,
lib/browserctl/commands/dialog.rb,
lib/browserctl/commands/resume.rb,
lib/browserctl/driver/cdp_page.rb,
lib/browserctl/recording/state.rb,
lib/browserctl/state/transport.rb,
lib/browserctl/commands/migrate.rb,
lib/browserctl/commands/storage.rb,
lib/browserctl/error/exit_codes.rb,
lib/browserctl/replay/telemetry.rb,
lib/browserctl/commands/snapshot.rb,
lib/browserctl/commands/workflow.rb,
lib/browserctl/workflow/promoter.rb,
lib/browserctl/commands/recording.rb,
lib/browserctl/driver/page_driver.rb,
lib/browserctl/encryption_service.rb,
lib/browserctl/recording/redactor.rb,
lib/browserctl/snapshot/annotator.rb,
lib/browserctl/snapshot/extractor.rb,
lib/browserctl/trace/event_stream.rb,
lib/browserctl/callable_definition.rb,
lib/browserctl/commands/cli_output.rb,
lib/browserctl/commands/screenshot.rb,
lib/browserctl/server/idle_watcher.rb,
lib/browserctl/server/page_session.rb,
lib/browserctl/snapshot/serializer.rb,
lib/browserctl/state/transports/s3.rb,
lib/browserctl/workflow/page_proxy.rb,
lib/browserctl/recording/log_writer.rb,
lib/browserctl/replay/snapshot_diff.rb,
lib/browserctl/secret_resolvers/env.rb,
lib/browserctl/server/handlers/data.rb,
lib/browserctl/server/handlers/hitl.rb,
lib/browserctl/snapshot/fingerprint.rb,
lib/browserctl/flows/stdlib/totp_2fa.rb,
lib/browserctl/secret_resolvers/base.rb,
lib/browserctl/server/handlers/state.rb,
lib/browserctl/state/transports/file.rb,
lib/browserctl/workflow/flow_wrapper.rb,
lib/browserctl/commands/output_format.rb,
lib/browserctl/contextual_persistence.rb,
lib/browserctl/detectors/auth_required.rb,
lib/browserctl/error/suggested_actions.rb,
lib/browserctl/server/handlers/cookies.rb,
lib/browserctl/server/handlers/storage.rb,
lib/browserctl/server/snapshot_builder.rb,
lib/browserctl/secret_resolver_registry.rb,
lib/browserctl/server/handlers/devtools.rb,
lib/browserctl/server/plugin_dispatcher.rb,
lib/browserctl/driver/ferrum_page_driver.rb,
lib/browserctl/server/command_dispatcher.rb,
lib/browserctl/workflow/promotion_ledger.rb,
lib/browserctl/workflow/recovery_manager.rb,
lib/browserctl/commands/passphrase_prompt.rb,
lib/browserctl/replay/fingerprint_matcher.rb,
lib/browserctl/server/handlers/navigation.rb,
lib/browserctl/commands/deprecation_notice.rb,
lib/browserctl/recording/workflow_renderer.rb,
lib/browserctl/server/handlers/interaction.rb,
lib/browserctl/server/handlers/observation.rb,
lib/browserctl/client/recording_interceptor.rb,
lib/browserctl/secret_resolvers/one_password.rb,
lib/browserctl/server/handlers/error_payload.rb,
lib/browserctl/state/transports/one_password.rb,
lib/browserctl/server/handlers/daemon_control.rb,
lib/browserctl/server/handlers/page_lifecycle.rb,
lib/browserctl/secret_resolvers/macos_keychain.rb
Overview
Pauses for a human to solve a Cloudflare challenge (Turnstile, “Just a moment…”, interactive checkbox), then verifies the challenge cleared before returning. Optionally saves the post-solve state under a name you can reload later with ‘state load`.
Reuses Browserctl::Detectors.cloudflare? — the server-side detector already shipped in v0.8 — by adapting the client-facing PageProxy to the duck-typed (current_url, body) interface the detector expects.
Defined Under Namespace
Modules: Commands, ContextualPersistence, CrashReport, Detectors, Driver, EncryptionService, Flows, FormatVersion, Migrations, OrphanSweeper, Policy, Replay, SecretResolvers, Snapshot, State, Trace, Tracing, Workflow Classes: AuthRequiredError, BrowserNotFound, CallableDefinition, Client, CommandDispatcher, DaemonUnavailableError, DomainNotAllowed, Error, Flow, FlowConditionDef, FlowContext, FlowError, FlowParamError, FlowPostconditionError, FlowPreconditionError, FlowRegistry, FlowStepError, HeaderlessLogDevice, IdleWatcher, JsonlFormatter, KeyNotFound, MultiLogger, PageNotFound, PageProxy, PageSession, PathNotAllowed, PluginCommand, PluginDispatcher, ProtocolMismatch, Recording, Redactor, RefNotFound, Runner, SecretResolverError, SecretResolverRegistry, SelectorNotFound, Server, SnapshotBuilder, StepResult, TimeoutError, WorkflowContext, WorkflowDefinition, WorkflowError
Constant Summary collapse
- DEFAULT_PLUGIN_TIMEOUT =
Default per-plugin command timeout (seconds). Plugins registered via register_command without an explicit ‘timeout:` are wrapped in this cap by `Browserctl::PluginDispatcher`. Pass `timeout: nil` on `register_command` to opt out (not recommended — a runaway plugin will hold the daemon’s command thread until the process is restarted).
30- FlowParamDef =
Back-compat aliases — flow_wrapper specs reference these directly.
CallableDefinition::ParamDef
- FlowStepDef =
CallableDefinition::StepDef
- LEVEL_MAP =
{ "debug" => ::Logger::DEBUG, "info" => ::Logger::INFO, "warn" => ::Logger::WARN, "error" => ::Logger::ERROR }.freeze
- LOG_SHIFT_AGE =
JSONL rotation policy. Stdlib ‘Logger` rotates by size when given an integer `shift_age` and `shift_size`.
10- LOG_SHIFT_SIZE =
keep last 10 rotated files
10 * 1024 * 1024
- VERSION =
"0.15.0"- WORKFLOW_FORMAT_VERSION =
Workflow-file format version. Workflows are Ruby files; the schema gate is a top-of-file comment header:
# format_version: 1Unlike bundles and recordings, an unsupported or missing version on a workflow file is a warning, not a hard failure. Workflows are human-authored Ruby — the loader prefers to surface drift via stderr and let the file run, rather than block execution. See docs/reference/format-versions.md.
1- SUPPORTED_WORKFLOW_FORMAT_VERSIONS =
[WORKFLOW_FORMAT_VERSION].freeze
- WORKFLOW_FORMAT_VERSION_HEADER =
Matches a leading-line comment of the form ‘# format_version: <int>`. Tolerates leading whitespace inside the comment body and ignores the `# frozen_string_literal: true` magic comment that conventionally precedes it.
/^\s*#\s*format_version:\s*(\d+)\s*$/- ParamDef =
Back-compat aliases — exposed in the public surface (flow_wrapper specs, workflow specs reference these directly).
CallableDefinition::ParamDef
- StepDef =
CallableDefinition::StepDef
- BROWSERCTL_DIR =
File.("~/.browserctl")
- IDLE_TTL =
30 * 60
- PROTOCOL_VERSION =
Increment when a breaking wire protocol change ships (new field names, removed commands, changed response shapes). Clients read this from ‘ping` to verify compatibility before sending commands.
"3"- SOCKET_PATH =
Backward-compatible constants
socket_path- PID_PATH =
pid_path
Class Method Summary collapse
- .all_daemon_names ⇒ Object
- .all_daemon_sockets ⇒ Object
- .build_jsonl_logger(level, component) ⇒ Object
-
.build_logger(level_name, log_path: nil, component: "daemon", jsonl: true) ⇒ Object
Build a logger that writes: - human-readable lines to stderr (unchanged behaviour) - human-readable lines to log_path: when given (the daemon tail file) - structured JSONL lines to ~/.browserctl/logs/<component>.log (rotating 10 files x 10MB) when jsonl: is true.
- .flow(name, &block) ⇒ Object
- .flow_registry_reset! ⇒ Object
- .flow_registry_snapshot ⇒ Object
-
.log_dir ⇒ Object
Resolved at call time so tests can override BROWSERCTL_DIR via stub_const.
- .log_path(name = nil) ⇒ Object
- .logger ⇒ Object
- .logger=(instance) ⇒ Object
- .lookup_flow(name) ⇒ Object
- .lookup_plugin_command(name) ⇒ Object
- .lookup_workflow(name) ⇒ Object
-
.next_daemon_name ⇒ Object
Returns nil when the default (unnamed) slot is free; otherwise returns “d1”, “d2”, etc.
-
.parse_workflow_format_version(source) ⇒ Object
Parses the ‘# format_version: N` header from a workflow file’s source.
- .pid_path(name = nil) ⇒ Object
- .plugin_commands_snapshot ⇒ Object
-
.register_command(name, timeout: DEFAULT_PLUGIN_TIMEOUT, &block) ⇒ Object
Registers a plugin command callable under ‘name`.
- .register_flow(flow) ⇒ Object
- .registry_snapshot ⇒ Object
- .socket_path(name = nil) ⇒ Object
-
.verify_workflow_format_version!(path) ⇒ Object
Reads a workflow file and warns to stderr when the ‘format_version:` header is missing or declares an unsupported version.
- .workflow(name) ⇒ Object
Class Method Details
.all_daemon_names ⇒ Object
38 39 40 41 |
# File 'lib/browserctl/constants.rb', line 38 def self.all_daemon_names all_daemon_sockets.map { |f| File.basename(f, ".sock") } .map { |n| n == "browserd" ? nil : n } end |
.all_daemon_sockets ⇒ Object
34 35 36 |
# File 'lib/browserctl/constants.rb', line 34 def self.all_daemon_sockets Dir[File.join(BROWSERCTL_DIR, "*.sock")] end |
.build_jsonl_logger(level, component) ⇒ Object
125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/browserctl/logger.rb', line 125 def self.build_jsonl_logger(level, component) dir = log_dir FileUtils.mkdir_p(dir, mode: 0o700) path = File.join(dir, "#{component}.log") device = HeaderlessLogDevice.new(path, shift_age: LOG_SHIFT_AGE, shift_size: LOG_SHIFT_SIZE) log = ::Logger.new(device) log.level = level log.progname = component log.formatter = JsonlFormatter.new(component: component) log rescue Errno::EACCES, Errno::ENOENT, Errno::EISDIR, Errno::ENOSPC, Errno::EROFS, IOError => e warn "browserctl: failed to build jsonl logger (#{e.class}: #{e.})" nil end |
.build_logger(level_name, log_path: nil, component: "daemon", jsonl: true) ⇒ Object
Build a logger that writes:
- human-readable lines to stderr (unchanged behaviour)
- human-readable lines to log_path: when given (the daemon tail file)
- structured JSONL lines to ~/.browserctl/logs/<component>.log (rotating
10 files x 10MB) when jsonl: is true
JSONL output is purely additive — existing stderr/stdout behaviour is preserved so scripted callers see no change.
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/browserctl/logger.rb', line 92 def self.build_logger(level_name, log_path: nil, component: "daemon", jsonl: true) level = LEVEL_MAP.fetch(level_name.to_s.downcase, ::Logger::INFO) text_formatter = proc do |sev, t, prog, msg| "#{t.strftime('%Y-%m-%dT%H:%M:%S')} #{sev[0]} [#{prog}] #{format_text_msg(msg)}\n" end loggers = [make_logger($stderr, level, text_formatter)] if log_path FileUtils.mkdir_p(File.dirname(log_path), mode: 0o700) FileUtils.touch(log_path) File.chmod(0o600, log_path) loggers << make_logger(log_path, level, text_formatter) end if jsonl jsonl_logger = build_jsonl_logger(level, component) loggers << jsonl_logger if jsonl_logger end loggers.length == 1 ? loggers.first : MultiLogger.new(*loggers) end |
.flow(name, &block) ⇒ Object
199 200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/browserctl/flow.rb', line 199 def self.flow(name, &block) unless block raise Browserctl::Error.new( "Browserctl.flow requires a block", code: Browserctl::Error::Codes::INVALID_DSL_USAGE, context: { dsl: :flow, action: :define, name: name.to_s } ) end flow = Flow.new(name).tap { |f| f.instance_exec(&block) } register_flow(flow) flow end |
.flow_registry_reset! ⇒ Object
225 226 227 |
# File 'lib/browserctl/flow.rb', line 225 def self.flow_registry_reset! @flow_registry_mutex.synchronize { @flow_registry.clear } end |
.flow_registry_snapshot ⇒ Object
221 222 223 |
# File 'lib/browserctl/flow.rb', line 221 def self.flow_registry_snapshot @flow_registry_mutex.synchronize { @flow_registry.dup } end |
.log_dir ⇒ Object
Resolved at call time so tests can override BROWSERCTL_DIR via stub_const.
22 23 24 |
# File 'lib/browserctl/logger.rb', line 22 def self.log_dir File.join(BROWSERCTL_DIR, "logs") end |
.log_path(name = nil) ⇒ Object
20 21 22 |
# File 'lib/browserctl/constants.rb', line 20 def self.log_path(name = nil) File.join(BROWSERCTL_DIR, name ? "#{name}.log" : "browserd.log") end |
.logger ⇒ Object
76 77 78 |
# File 'lib/browserctl/logger.rb', line 76 def self.logger @logger ||= build_logger("info") end |
.logger=(instance) ⇒ Object
80 81 82 |
# File 'lib/browserctl/logger.rb', line 80 def self.logger=(instance) @logger = instance end |
.lookup_flow(name) ⇒ Object
217 218 219 |
# File 'lib/browserctl/flow.rb', line 217 def self.lookup_flow(name) @flow_registry_mutex.synchronize { @flow_registry[name.to_s] } end |
.lookup_plugin_command(name) ⇒ Object
45 46 47 |
# File 'lib/browserctl.rb', line 45 def self.lookup_plugin_command(name) @plugin_commands_mutex.synchronize { @plugin_commands[name.to_s] } end |
.lookup_workflow(name) ⇒ Object
265 266 267 |
# File 'lib/browserctl/workflow.rb', line 265 def self.lookup_workflow(name) @registry_mutex.synchronize { @registry[name.to_s] } end |
.next_daemon_name ⇒ Object
Returns nil when the default (unnamed) slot is free; otherwise returns “d1”, “d2”, etc.
25 26 27 28 29 30 31 32 |
# File 'lib/browserctl/constants.rb', line 25 def self.next_daemon_name return nil unless File.exist?(socket_path) 1.upto(99) do |i| return "d#{i}" unless File.exist?(socket_path("d#{i}")) end raise Browserctl::Error, "too many running daemons (limit: 99)" end |
.parse_workflow_format_version(source) ⇒ Object
Parses the ‘# format_version: N` header from a workflow file’s source. Scans only the contiguous leading comment block (and blank lines) so the header cannot be smuggled in mid-file. Returns the integer if present, or nil if the file has no version header.
37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/browserctl/workflow.rb', line 37 def self.parse_workflow_format_version(source) source.each_line do |line| stripped = line.strip next if stripped.empty? break unless stripped.start_with?("#") if (m = line.match(WORKFLOW_FORMAT_VERSION_HEADER)) return Integer(m[1]) end end nil end |
.pid_path(name = nil) ⇒ Object
16 17 18 |
# File 'lib/browserctl/constants.rb', line 16 def self.pid_path(name = nil) File.join(BROWSERCTL_DIR, name ? "#{name}.pid" : "browserd.pid") end |
.plugin_commands_snapshot ⇒ Object
49 50 51 |
# File 'lib/browserctl.rb', line 49 def self.plugin_commands_snapshot @plugin_commands_mutex.synchronize { @plugin_commands.dup } end |
.register_command(name, timeout: DEFAULT_PLUGIN_TIMEOUT, &block) ⇒ Object
Registers a plugin command callable under ‘name`. The block receives `(session, req)` and must return a JSON-RPC-shaped Hash. The daemon wraps every invocation in a per-plugin timeout (default DEFAULT_PLUGIN_TIMEOUT) and a rescue boundary that converts any uncaught exception into a typed `PLUGIN_FAILED` response without taking down the daemon — see PluginDispatcher.
39 40 41 42 43 |
# File 'lib/browserctl.rb', line 39 def self.register_command(name, timeout: DEFAULT_PLUGIN_TIMEOUT, &block) @plugin_commands_mutex.synchronize do @plugin_commands[name.to_s] = PluginCommand.new(block: block, timeout: timeout) end end |
.register_flow(flow) ⇒ Object
213 214 215 |
# File 'lib/browserctl/flow.rb', line 213 def self.register_flow(flow) @flow_registry_mutex.synchronize { @flow_registry[flow.name] = flow } end |
.registry_snapshot ⇒ Object
269 270 271 |
# File 'lib/browserctl/workflow.rb', line 269 def self.registry_snapshot @registry_mutex.synchronize { @registry.dup } end |
.socket_path(name = nil) ⇒ Object
12 13 14 |
# File 'lib/browserctl/constants.rb', line 12 def self.socket_path(name = nil) File.join(BROWSERCTL_DIR, name ? "#{name}.sock" : "browserd.sock") end |
.verify_workflow_format_version!(path) ⇒ Object
Reads a workflow file and warns to stderr when the ‘format_version:` header is missing or declares an unsupported version. Always returns the parsed integer (or nil) — never raises. Callers should still `load` the file regardless.
54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/browserctl/workflow.rb', line 54 def self.verify_workflow_format_version!(path) source = File.read(path) version = parse_workflow_format_version(source) if version.nil? warn "[browserctl] workflow #{path} is missing a `# format_version: N` header " \ "(expected #{WORKFLOW_FORMAT_VERSION}); proceeding anyway" elsif !SUPPORTED_WORKFLOW_FORMAT_VERSIONS.include?(version) warn "[browserctl] workflow #{path} format_version=#{version} is not supported " \ "(expected #{WORKFLOW_FORMAT_VERSION}); proceeding anyway" end version end |
.workflow(name) ⇒ Object
259 260 261 262 263 |
# File 'lib/browserctl/workflow.rb', line 259 def self.workflow(name, &) defn = WorkflowDefinition.new(name.to_s) defn.instance_exec(&) @registry_mutex.synchronize { @registry[name.to_s] = defn } end |