Skip to content

Add run_on_bind plugin for bind-triggered command execution - #899

Open
glowwserrano7 wants to merge 6 commits into
mainfrom
fetch-web-add-run-command
Open

Add run_on_bind plugin for bind-triggered command execution#899
glowwserrano7 wants to merge 6 commits into
mainfrom
fetch-web-add-run-command

Conversation

@glowwserrano7

@glowwserrano7 glowwserrano7 commented Jul 10, 2026

Copy link
Copy Markdown

Summary

Adds run_on_bind, a standalone plugin that executes host or guest commands when a guest
service binds a port exposed to the host by the VPN plugin.

This started as an extension to fetch_web. It is split into its own plugin so that:

  • fetch_web remains responsible only for detecting web services and retrieving content.
  • Bind-triggered execution works for any service, not just HTTP/HTTPS.
  • Command execution can be tested independently of web fetching.

Changes

New: run_on_bind

  • Subscribes to the VPN on_bind event and filters by proto, ports, and an optional
    endpoints allowlist. No longer limited to ports 80/443.
  • De-duplicates per endpoint, and drains a queue from a single worker thread so two
    simultaneous binds can never produce concurrent guest_cmd.py invocations.
  • Host mode: runs on the host (inside the container), with the working directory set to
    penguin's proj_dir argument.
  • Guest mode (default): runs inside the emulated guest via guest_cmd.py. Requires
    core.guest_cmd: true.
  • delay (default 20s) before running, and a per-command timeout (default 120s) so an
    unresponsive guest cannot hang the run.
  • An attempt succeeds if any configured command succeeds. Configurations routinely
    include commands that only apply to some targets, so requiring all of them to pass would
    keep retrying endpoints on account of commands that were never going to work.
  • A failed attempt is not terminal: the next endpoint to bind gets its own try, uncapped.
    This keeps a run from wedging when the first endpoint to appear (often a loopback bind)
    isn't the one that works. The first successful attempt is the last.
  • shutdown_after_cmd ends the emulation after the first successful attempt.
  • Writes run_on_bind_output.txt — one block per command per attempt, recording endpoint,
    attempt, mode, command, Status: SUCCESS|FAILED, return code, stdout and stderr. Every
    command is recorded regardless of outcome, including timeouts and missing executables.

New: run_on_bind_testing

  • host_marker: a file a host-mode command is expected to create. Relative paths resolve
    against proj_dir — the same working directory host commands run in — so a command
    writing a relative path and this check agree on where the file lands. results/… resolves
    against the output directory; absolute paths are used as-is.
  • output_contains: strings expected in run_on_bind_output.txt.
  • wait_timeout (default 30): polls for each expected file rather than sleeping a fixed
    interval, so a passing run finishes as soon as the files appear.
  • Writes results to run_on_bind_test.txt.

fetch_web_testing scoped back to fetch_web only

Removed cmd_on_bind_marker, cmd_on_bind_guest_output_contains, and cmd_wait_timeout;
those checks now live in run_on_bind_testing. Web-service verification (the
web_<ip>_<port> output files) is unchanged.

Example

core:
  guest_cmd: true
  timeout: 600

plugins:
  run_on_bind:
    ports: [80]
    commands:
      - mode: host
        run:
          - touch hello.txt
      - mode: guest
        run:
          - echo "im here" > /tmp/temp.txt && cat /tmp/temp.txt
    shutdown_after_cmd: true

  run_on_bind_testing:
    host_marker: hello.txt
    output_contains:
      - here
    wait_timeout: 30

This verifies that host-mode commands create the expected marker file and that guest-mode
commands execute and produce the expected output.

commands also accepts a bare string or a list of strings, both of which default to guest
mode:

  run_on_bind:
    commands: python3 example.py

Running alongside fetch_web

The two plugins share no state — separate subscriptions, queues, worker threads and output
files — but either can end the emulation. When both are loaded, enable at most one shutdown
flag (shutdown_after_cmd, shutdown_after_www, shutdown_on_failure); otherwise whichever
finishes first calls end_analysis() and truncates the other's in-flight work. To let both
complete, leave all three unset and bound the run with core.timeout, which is otherwise
unbounded.

@glowwserrano7
glowwserrano7 requested a review from zestrada July 10, 2026 18:03

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lintly has detected code quality issues in this pull request.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lintly has detected code quality issues in this pull request.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lintly has detected code quality issues in this pull request.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the existing fetch_web actuation plugin to optionally execute post-bind commands (host or guest) after a web service bind is detected, and adds a new fetch_web_testing plugin to validate both fetched web output and cmd-on-bind side effects/output.

Changes:

  • Add cmd_on_bind support to fetch_web, including host/guest execution modes and optional shutdown after commands.
  • Persist guest command output to guest_commands_output.txt for later verification.
  • Introduce fetch_web_testing plugin to verify fetch outputs, host marker files, and expected guest command output.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
pyplugins/actuation/fetch_web.py Adds cmd-on-bind parsing/execution, host/guest modes, optional shutdown behavior, and guest-output capture.
pyplugins/testing/fetch_web_testing.py New integration-test plugin to verify fetch outputs and validate cmd-on-bind host/guest outcomes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pyplugins/actuation/fetch_web.py Outdated
Comment on lines +199 to +209
# Only trigger cmd_on_bind for 0.0.0.0 to avoid running commands
# multiple times
if self.cmd_on_bind_structured and guest_ip == "0.0.0.0":
self.logger.info(
f"Bind detected on {guest_ip}:{guest_port}, spawning cmd_on_bind thread")
t = threading.Thread(
target=self._delayed_bind_workflow, args=(guest_ip, guest_port)
)
t.daemon = True
t.start()

Comment thread pyplugins/actuation/fetch_web.py Outdated
Comment on lines +275 to +288
# Determine working directory for host mode
if self.outdir:
cwd = os.path.abspath(
os.path.join(self.outdir, "../.."))
self.logger.info(
f"Derived project root from outdir: {cwd}")
if not os.path.isdir(cwd):
self.logger.warning(
f"Project root does not exist: {cwd}"
)
cwd = os.getcwd()
else:
cwd = os.getcwd()

Comment thread pyplugins/actuation/fetch_web.py Outdated
Comment on lines +123 to +146
if self.cmd_on_bind is not None:
# Check if it's list of dicts with mode/cmd
if isinstance(
self.cmd_on_bind, list) and len(
self.cmd_on_bind) > 0:
if isinstance(self.cmd_on_bind[0], dict):
self.cmd_on_bind_structured = self.cmd_on_bind
self.logger.info(
"Using structured cmd_on_bind with mode specification"
)
else:
# List of strings (backward compatibility)
self.cmd_on_bind_structured = [
{"mode": "guest", "cmd": [str(c)]} for c in self.cmd_on_bind
]
self.logger.info("Converting legacy cmd_on_bind format")
elif isinstance(self.cmd_on_bind, str):
# Single string command (backward compatibility)
self.cmd_on_bind_structured = [
{"mode": "guest", "cmd": [str(self.cmd_on_bind)]}
]
self.logger.info("Converting single string cmd_on_bind")
else:
self.cmd_on_bind_structured = []
Comment thread pyplugins/testing/fetch_web_testing.py Outdated
Comment on lines +33 to +36
self.cmd_on_bind_marker = self.get_arg("cmd_on_bind_marker")
self.cmd_on_bind_guest_output_contains = self.get_arg(
"cmd_on_bind_guest_output_contains")
self.cmd_wait_timeout = int(self.get_arg("cmd_wait_timeout") or 30)
Comment thread pyplugins/testing/fetch_web_testing.py Outdated
Comment on lines +127 to +129
project_root = os.path.abspath(
os.path.join(self.outdir, "../.."))
marker_path = os.path.join(project_root, marker)
Comment thread pyplugins/actuation/fetch_web.py Outdated
Comment on lines +148 to +150
# Guest-cmd must be enabled if any guest commands exist
has_guest_cmds = any(
entry.get("mode") == "guest" for entry in self.cmd_on_bind_structured)
@glowwserrano7
glowwserrano7 force-pushed the fetch-web-add-run-command branch from 0b32794 to 0af50d8 Compare August 7, 2026 17:46

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lintly has detected code quality issues in this pull request.

@glowwserrano7 glowwserrano7 changed the title Add cmd_on_bind functionality to fetch_web plugin & testing plugin Add run_on_bind plugin for bind-triggered command execution Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants