Skip to content

feat(agent): Albacore lifecycle fixes plus an out-of-process plugin framework#164

Merged
nfebe merged 4 commits into
mainfrom
enh/albacore/proxy-compose-lifecycle
Jun 30, 2026
Merged

feat(agent): Albacore lifecycle fixes plus an out-of-process plugin framework#164
nfebe merged 4 commits into
mainfrom
enh/albacore/proxy-compose-lifecycle

Conversation

@nfebe

@nfebe nfebe commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Albacore (#158)

  • Relative env_file resolves against the deployment directory, so setting a service image or updating compose stops failing validation.
  • Extra domains route to their own deployment's container instead of an arbitrary one sharing a service name; long hostnames stop breaking nginx on upgraded installs.
  • Deployment and service actions can force-recreate, rebuild without cache, and pull fresh, so updated env vars and images take effect.
  • A deployment's primary service can be pinned and shows per service.

Plugins

  • Plugins can be standalone binaries the agent launches and reverse-proxies, so an app ships its own API and UI without being compiled in or run as a container.
  • Capability-split plugin interface; the built-in firewall and DNS providers register through one registry and list uniformly.
  • The marketplace catalog is fetched through the agent rather than the browser; upstream configurable.

Firewall models and validates rules but does not enforce them yet.

nfebe added 2 commits June 30, 2026 01:30
Setting a service image or updating compose now validates a relative env_file
against the deployment directory, so deployments using ./.env no longer fail
validation.

Additional domains route to the deployment's own container instead of an
arbitrary container that happens to share a service name, fixing intermittent
cross-deployment responses. Long hostnames no longer break nginx after an
upgrade now that the hash sizing is applied to managed configs.

Deployment and service actions can force-recreate containers, rebuild without
cache, and pull fresh images, so updated environment variables and images
actually take effect without dropping to the terminal.

A deployment's primary service can be pinned and is reported per service, and
the pin survives compose updates and re-discovery.

Adds Access Groups, a built-in app that models per-deployment east-west and
egress rules. Rules are persisted and previewable, with enforcement still to
come.
…n apps

Plugins can now be standalone binaries that the agent launches as subprocesses
and reverse-proxies, so an app ships its own API and UI without being compiled
into the agent or run as a container.

The plugin contract is split into small capability interfaces, so an app
implements only what it needs: a firewall has no services to start or stop. The
built-in apps, a host firewall and the DNS providers, now register through one
registry and list uniformly alongside any installed plugins.

The marketplace catalog is fetched through the agent rather than the browser,
which removes the cross-origin restriction that blocked it; the upstream is
configurable.

Drops the per-deployment access-groups app, which only duplicated the network
isolation Docker already provides between deployments by default.
@sourceant

sourceant Bot commented Jun 30, 2026

Copy link
Copy Markdown

Code Review Summary

This PR introduces a robust out-of-process plugin framework (Albacore) and several lifecycle fixes for deployments. It transitions from a monolithic plugin interface to a capability-based one, allowing for more flexible service extensions like the new Firewall module.

🚀 Key Improvements

  • Out-of-process plugin execution via Unix sockets.
  • Capability-based plugin registry (Lifecycle, Routable, Widget).
  • Enhanced Compose validation that respects relative env_file paths.
  • Automatic Nginx configuration refreshes on agent upgrade.

💡 Minor Suggestions

  • Use subtle.ConstantTimeCompare for handshakes.
  • Prefix plugin logs to distinguish output in the host process.

@sourceant sourceant 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.

Review complete. See the overview comment for a summary.

pluginapi.EnvAgentURL+"="+h.agentURL,
pluginapi.EnvToken+"="+h.token,
)
cmd.Stdout = os.Stdout

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redirecting both Stdout and Stderr to the parent process's Stdout can make debugging plugin crashes difficult as error messages will be interleaved with standard logs without distinction. Consider using a prefixed logger.

Suggested change
cmd.Stdout = os.Stdout
cmd.Stdout = log.Writer()
cmd.Stderr = log.Writer()

if resp.StatusCode != http.StatusOK {
return pluginapi.Info{}, fmt.Errorf("info returned %d", resp.StatusCode)
}
if resp.Header.Get(pluginapi.HandshakeHeader) != handshake {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The handshake check uses a simple string comparison. While sufficient for the current implementation, using subtle.ConstantTimeCompare is a better security practice for sensitive handshake/token comparisons to prevent timing attacks.

Suggested change
if resp.Header.Get(pluginapi.HandshakeHeader) != handshake {
if subtle.ConstantTimeCompare([]byte(resp.Header.Get(pluginapi.HandshakeHeader)), []byte(handshake)) != 1 {

return out, err
}
}
args := applyUpFlags([]string{"up", "-d", "--no-deps"}, ro)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

When fresh_pull is requested, appending --pull always is correct, but ensure this doesn't conflict with existing logic in runCompose if it also tries to manage pull flags.

Suggested change
args := applyUpFlags([]string{"up", "-d", "--no-deps"}, ro)
args = applyUpFlags([]string{"up", "-d", "--no-deps", "--pull", "always"}, ro)

An additional domain that points at a deployment's own primary service produced
a proxy upstream with the deployment name repeated, which does not resolve, so
the deployment became unreachable and requests timed out. Such a domain now
routes to the correct container.

@sourceant sourceant 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.

Review complete. See the overview comment for a summary.

if resp.StatusCode != http.StatusOK {
return pluginapi.Info{}, fmt.Errorf("info returned %d", resp.StatusCode)
}
if resp.Header.Get(pluginapi.HandshakeHeader) != handshake {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

While existing review comments suggested subtle.ConstantTimeCompare, a simple equality check is still being used here. For a per-launch random handshake cookie, standard equality is likely fine, but ensure the HandshakeHeader value is not empty to avoid accidental matches if a plugin misbehaves.

Suggested change
if resp.Header.Get(pluginapi.HandshakeHeader) != handshake {
if h := resp.Header.Get(pluginapi.HandshakeHeader); h == "" || h != handshake {

Comment thread pkg/pluginsdk/sdk.go
}

// A stale socket file from a previous run would block Listen.
_ = os.Remove(socket)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The plugin SDK removes the socket file before listening. This is good practice for Unix sockets, but consider checking if the file exists and is indeed a socket to avoid accidentally deleting unrelated files if the environment variable is misconfigured.

Suggested change
_ = os.Remove(socket)
if fi, err := os.Stat(socket); err == nil && fi.Mode()&os.ModeSocket != 0 {
_ = os.Remove(socket)
}

@sourceant sourceant 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.

Review complete. No specific code suggestions were generated. See the overview comment for a summary.

@nfebe nfebe merged commit a0f5a2a into main Jun 30, 2026
5 checks passed
@nfebe nfebe deleted the enh/albacore/proxy-compose-lifecycle branch June 30, 2026 23:56
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.

1 participant