Experiment: HTTP protocol for fins - #395
Draft
hidde-jan wants to merge 22 commits into
Draft
Conversation
Drop the MQTT-based Fin capability, its controller, and the associated message/protocol models, along with the ENABLE_FINS wiring, mocks, and tests. See docs/adr/FIN-EXTERNAL-INTEGRATION-ANALYSIS.md for why: a push-based, broker-mediated protocol turned out to be fragile and hard to reason about for job delivery/ack semantics, retries, and job ownership. No transitional adaptation of the MQTT capability to the stepwise Context is kept here since it's being deleted outright - only the final, MQTT-free state matters for review. Also drops the paho.mqtt.golang dependency and the mosquitto broker config/deployment (docker-compose, Dockerfile config) that only existed to support it. The replacement pull-based HTTP/JSON protocol is proposed in docs/adr/FIN-WEBHOOK-PROTOCOL-PROPOSAL.md and implemented in a later, separate series of commits. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop the MQTT-based Fin protocol/setup docs and the MQTT Fin example playbook, and finish scrubbing stale MQTT references from the remaining docs (README, concepts, core-components, getting-started, installation-configuration, soarca-extensions). Fin documentation for the new HTTP/JSON protocol will be added alongside its implementation in a later commit series. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move capability execution from a pairwise (command x target) model to a stepwise one, and fix capability routing to use the CACAO-correct identifier field. Stepwise dispatch: - Replace capability.Context's singular Command/Target/Authentication fields with Commands []cacao.Command and Targets []ResolvedTarget (a target paired with its own resolved auth info, since different targets in a step may legitimately use different authentication). - The action executor now calls a capability once per step, passing the step's full commands[]/targets[] arrays, instead of once per (command, target) pair. This fixes the case where a step has zero targets: previously nothing ran at all (the pairwise loop had nothing to iterate), which silently violated CACAO's single-outcome-per-step model; now a step always produces exactly one outcome. - Adapt every native capability (ssh, http, powershell, openc2, manual) to the new Context shape, each internalizing its own commands/targets iteration. ssh, http, powershell, and openc2 all perform commands against a target, so a step declaring zero targets for one of these has nothing to run against: they skip execution (no commands run, empty result, nil error) rather than fabricating a dummy target, matching prior single-target behavior for the common case. manual is target-independent (a human ticket resolved once per step, not per target) and is unaffected by target count. Routing by agent.Type: - CACAO documents agent.type as the identifier field and agent.name as display-only, but the executor routed on agent.Name (executor.capabilities[agent.Name]) and never read Type at all. Fix: route strictly on agent.Type. Each capability keeps registering under its own distinct type value (soarca-ssh, soarca-http-api, soarca-openc2-http, soarca-powershell, soarca-manual) - a step routes to exactly one capability via its agent's type, matching the chosen one-command-type-per-step granularity rather than introducing a second, internal dispatch-by-commands[].type mechanism alongside it. - Fix examples/manual-playbook.json, which had drifted to an unused second agent definition. Update executer.md to describe the corrected routing model and clarify that `name` is a free-text label with no role in routing. This lands ahead of the new HTTP/JSON Fin protocol, which needs both: one call per step (matching planned Fin job granularity), and agent type to be a meaningful, real routing key so a Fin capability pool can be addressed by its own distinct type value. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
StepId is stable across re-executions of the same step (while-loops, cyclic on_completion graphs), but the reporting cache and public API keyed step results by StepId alone. A second start-report for the same StepId was rejected and dropped (fire-and-forget IStepReporter calls), silently losing data whenever a step ran more than once in the same execution. Add StepExecutionId (a fresh UUID minted once per step invocation) to execution.Metadata. The decomposer's new newStepMetadata() helper is the single mint point, called for every step dispatch including each while-loop iteration. Thread the field through: - IStepReporter / IDownStreamReporter: ReportStepStart/ReportStepEnd now take the full execution.Metadata instead of a bare executionId. - Reporting cache (downstream_reporter/cache): StepResults is now keyed by StepExecutionId instead of StepId, so re-executed steps get their own entry instead of colliding. - Public API (StepExecutionReport / reporter_parser): exposes the new field and the corrected keying. - TheHive integration: updated for interface conformance only (no deeper use of StepExecutionId in TheHive's own data model yet). The manual capability's interaction registry deliberately keeps keying pending commands on (ExecutionId, StepId) rather than StepExecutionId -- one pending manual command per step is intentional and unrelated to this cache bug. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
New pkg/core/registry provides Registry[V], a small, generic, concurrency-safe, two-level (outerKey -> innerKey -> value) in-memory store: Register/Get/Remove/List, with automatic pruning of an outer key once it has no remaining inner entries. This generalizes the register/lookup/expire pattern already used by the manual-command interaction queue (keyed by execution id -> step id), so the next commit can refactor that queue to use it instead of its own hand-rolled nested map, and the future Fin job queue (keyed by capability type -> job id) can reuse it too instead of duplicating the same bookkeeping. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-execution race, switch continue to PUT
Implements the manual capability changes specified in
docs/adr/EXECUTION-MODEL.md: fixes the pending-interaction race between
Execute() and its async cleanup goroutine, rekeys the pending-interaction
registry from (ExecutionId, StepId) to (ExecutionId, StepExecutionId),
and replaces the generic POST /manual/continue endpoint with a PUT on
the specific pending-command resource.
Re-execution race fix:
- ManualCapability.Execute() registered a pending interaction and
blocked on user input, while the entry's removal happened in a
separate goroutine reacting to ctx.Done(), with nothing guaranteeing
removal completed before Execute() returned. A step re-executed
quickly after the previous invocation returned (e.g. the next
iteration of a while-condition loop body) could collide with its own
predecessor's not-yet-finished cleanup. Fixed by adding a synchronous
Deregister() call right after the blocking wait resolves, keeping the
async cleanup goroutine as an idempotent backstop.
StepExecutionId rekeying:
- The registry's "one pending manual command per StepId" behaviour was
an accident of implementation - StepId was simply the only
per-invocation identifier available - not a deliberate design
constraint. It broke down as soon as a step could legitimately be
invoked more than once with independently-pending state: sequential
re-executions (while-condition loops, cyclic on_completion graphs)
or, once implemented, parallel branches converging on the same step.
- pkg/models/api/manual.go: added StepExecutionId to
InteractionCommandData (StepId kept for display).
- pkg/core/capability/manual/interaction/interaction.go: register,
look up, and remove pending commands by StepExecutionId instead of
StepId.
- New test proving two pending commands with the same StepId but
different StepExecutionId coexist and resolve independently.
POST /manual/continue -> PUT /manual/{exec_id}/{step_execution_id}:
- Resolving a pending command is now a PUT on the same resource GET
identifies, instead of a generic RPC-style endpoint duplicating its
own ids in the body. ManualOutArgsUpdatePayload drops
ExecutionId/PlaybookId/StepId/StepExecutionId entirely - the path is
the sole source of truth for which command is being resolved.
- pkg/api/api.go: route changed accordingly.
- pkg/api/manual/manual_api.go: PostContinue renamed to PutContinue;
parses exec_id/step_execution_id from the path (mirroring
GetPendingCommand), looks up the pending command first (clean 404 for
an unknown resource, and supplies PlaybookId for the response without
needing it in the body), then hands the full, looked-up
execution.Metadata to the storage layer's PostContinue.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ndpoint
Update the hand-written manual API reference and regenerate Swagger
docs (`make swagger swagger-docs`) to reflect the previous commit's
manual capability changes:
- docs/content/en/docs/core-components/api-manual.md: describes
GET/PUT /manual/{exec_id}/{step_execution_id} (replacing
{step-id} and POST /manual/continue), the smaller PUT request body,
and why a pending command may share a step_id across invocations.
- docs/content/en/docs/core-components/modules.md: updates the
protocol diagram to the same routes (also fixes a pre-existing
{step-id} staleness left over from the earlier StepExecutionId work).
- docs/static/openapi/{swagger.json,swagger.yaml}: regenerated from the
updated Swagger annotations.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduce pkg/models/fin, the data models for the new HTTP/JSON pull-based Fin protocol from docs/adr/FIN-WEBHOOK-PROTOCOL-PROPOSAL.md, replacing the MQTT-based one removed earlier on this branch. This is models only (no registry, queue, API, or capability wiring yet) — those follow in later commits. - Capability: one executable capability a Fin declares at registration (Type is the routing key playbook authors reference; Description/ Version/StepExample are display/documentation only). - Record: the persisted (MongoDB-backed) view of a registered Fin — FinId, a hash of its fin_token (never the plaintext credential), declared Capabilities, and RegisteredAt/LastSeen. Fin registrations are meant to survive a SOARCA restart, unlike the job queue (planned in-memory, since execution state itself is not persisted/resumed today either). - Job/StepInfo/Command/Target: the poll-response payload a Fin receives — one job per step, commands[]/targets[] as arrays (0, 1, or many targets), each target carrying its own resolved authentication. Adds StepExecutionId (not in the original proposal draft) so Fin jobs key consistently with the rest of the execution model. - JobResult/TargetResult/JobState: the result a Fin submits — a single aggregated (state, variables) pair that SOARCA's step machinery consumes, plus optional per-target diagnostic detail for reporting/audit only. - RegisterRequest/Response, PollRequest/Response, ResultRequest, StatusPingRequest/Response, ListResponse: the wire-level request/ response shapes for the register/poll/result/status-ping/list endpoints, matching the proposal's documented JSON payloads. - Sentinel error types for the auth/lookup failure modes the protocol needs to distinguish (invalid registration/fin token, unknown fin/job, job not leased to the calling fin). Includes JSON-shape regression tests pinning the wire contract against the documented protocol payloads.
Introduce the two data-holding services the new HTTP/JSON Fin protocol needs, per the design decision that Fin registrations persist across a SOARCA restart but the job queue does not (matching the fact that in-flight execution state itself isn't persisted/resumed either): - internal/database/fin: IFinRepository, the persistence contract for fin.Record (register/get/find-by-token-hash/list/touch/unregister), implemented against the existing generic database.Database interface so it works unmodified against MongoDB. Extends internal/database/mongodb's dbtypes union and adds a fin_registry_collection alongside the existing playbook collection. - internal/database/finmemory: a non-persisted fallback implementation of the same interface for running without a configured database, mirroring internal/database/memory's existing playbook fallback. - pkg/core/capability/fin/queue: the in-memory Fin job queue. A future FinCapability will Enqueue one Job per step invocation and block (bounded by the step's own deadline) for a result; independently polling Fin processes registered under the job's CapabilityType Claim it via long-poll, Submit a result, or ExtendLease via a status ping. Expired leases (a Fin that stopped polling/responding) are swept and requeued in the background so another Fin registered under the same type can pick the job up. No HTTP endpoints or capability wiring yet - those follow in later commits. All new code has unit test coverage (the repository tests use a hand-rolled database.Database test double rather than a live MongoDB, consistent with there being no existing unit tests of the MongoDB-backed playbook repository either).
Implement the REST surface of the new pull-based Fin protocol on top
of the registry/queue services added previously, wiring pkg/models/fin
directly as the request/response wire types (no separate api-package
DTOs needed - that package was already designed as the wire format):
- POST /fin/register registration-token gated; issues a
new fin_id/fin_token pair
- POST /fin/poll fin-token gated; long-polls the job
queue for a job matching the calling
Fin's registered capability types,
204 No Content on timeout
- PUT /fin/jobs/:job_id fin-token gated; submits a job's
result, only if leased to the caller
- PATCH /fin/jobs/:job_id/status fin-token gated; extends a job's
lease (status-ping liveness/lease
renewal, §2.4/§2.5); the response
shape has room for a future
cancellation instruction, always
empty for now since cancellation
itself isn't implemented yet
- DELETE /fin/:fin_id fin-token gated; a Fin may only
delete its own registration
- GET /fin/, GET /fin/:fin_id admin/dashboard discovery reads,
not Fin-authenticated
Adds pkg/core/capability/fin/token for fin_token generation/hashing/
constant-time comparison (registration tokens and fin_tokens are only
ever compared/looked-up by hash, never stored or logged in plaintext).
RequireFinToken is a route-group-scoped gin middleware (not global),
resolving the Authorization: Bearer token to its fin.Record via
IFinRepository.FindByTokenHash and making it available to handlers -
deliberately kept separate from the existing JWT-role-based gauth
middleware used for the rest of the API, since Fins authenticate with
their own shared-secret scheme, not user JWTs. Wiring this handler and
its config (FIN_REGISTRATION_TOKEN, poll/lease defaults) into
controller.go is a later stage.
Regenerates docs/static/openapi/swagger.json/.yaml for the new
annotated endpoints/types.
Full unit test coverage over the handlers (registration, auth
middleware, poll/claim/submit/status-ping round trips and their
failure modes, unregister ownership check, list/get) using the
in-memory repository fallback and the real in-memory queue.
Implements capability.ICapability on top of the Fin job queue: Execute converts a step invocation into one fin.Job (commands, targets+auth, step metadata, a lease sized off the step's own timeout) and blocks on queue.Queue.Enqueue until a claiming Fin submits a result or the deadline elapses. A JobResult with State == JobStateFailure is turned into a Go error so it still flows through the executor's normal on_completion/on_failure branching. Fin capability types are declared dynamically at Fin registration time, so they can't be pre-populated into the executor's static capabilities map the way builtins are. Rather than complicating that map, Executor grows an optional finFallback capability (Executor.SetFinFallback), consulted whenever agent.Type doesn't match any statically-registered capability - FinCapability is wired as that fallback, and reads capability.Context's new Agent field to know which capability type to route the job under. Passing no fallback (the default) preserves today's exact "capability ... is not available in soarca" error behavior, so existing callers are unaffected until Fin support is actually wired up in the next stage.
…tes) - Controller gains a finRepo, toggled between Mongo (SetupFinRepository) and in-memory (finmemory.New()) alongside the existing playbookRepo, using the same DATABASE env var. - A package-level mainFinQueue (queue.Queue) is shared across every action.Executor built by NewDecomposer() (one per execution) and the Fin API handlers, so all Fin jobs land in one queue regardless of which execution enqueued them. - NewDecomposer() wires fincapability.New(mainFinQueue, guid) into the action executor via SetFinFallback, so any agent.Type not matching a built-in capability routes to a registered Fin declaring that type. - initializeCore() reorders setupDatabase() before intializeAuthenticationMiddleware() and registers routes.FinPublic before the admin auth middleware (Fins authenticate via fin_token, not JWT) with a warning comment against reordering; routes.FinAdmin is registered alongside the rest of the admin-gated API. - New FIN_REGISTRATION_TOKEN/FIN_POLL_INTERVAL_SECONDS/ FIN_LONG_POLL_TIMEOUT_SECONDS/FIN_JOB_LEASE_SECONDS env vars replace the removed ENABLE_FINS/MQTT_BROKER/MQTT_PORT entries in .env.example. - Verified end-to-end with a real running binary (in-memory DB): POST /fin/register -> 201 with fin_id/fin_token/poll config, GET /fin (admin) lists the registered Fin. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- New docs/content/en/docs/soarca-extensions/fin-protocol.md replaces the removed MQTT-based protocol page: endpoints table, auth model (registration token vs fin_token vs admin JWT), registration/poll/ result/status-ping/unregister/discovery walkthroughs with example payloads, lease/retry semantics, and a sequence diagram. Links from soarca-extensions/_index.md. - New examples/fin-playbook.json: a worked playbook combining a native SSH capability step with a step routed to an externally-registered Fin capability (agent type custom-ssh-fin), demonstrating how the two coexist in one playbook. - Verified end-to-end against a live running binary: registered a fin, submitted examples/fin-playbook.json, triggered it, polled and claimed the Fin-routed job, submitted a result, and confirmed both the step and execution report as successfully_executed. - Fixed two small pre-existing inaccuracies noticed while documenting the protocol: pkg/models/fin/protocol.go doc comments referenced stale /api/v1/fins/* paths instead of the actual registered /fin/* routes, and fin.Record.LastSeen's doc comment overclaimed active TTL-based expiry from routing that isn't implemented - stale Fins are simply never polling, so jobs queued under their capability types go unclaimed until the step's own timeout elapses (the job lease mechanism, not LastSeen, is what actually bounds that wait). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
New pkg/api/fin/fin_e2e_test.go (package fin_test, avoiding an import
cycle) wires the real fincapability.Capability, a real queue.Queue, and
the real FinHandler HTTP routes together the same way
internal/controller/controller.go wires them in production, with no
mocks standing in for either side of the protocol.
TestFullFinProtocolFlow drives the whole lifecycle this protocol exists
for: register a fin declaring a capability type -> a concurrently
running Execute() call enqueues a job under that type -> POST /fin/poll
claims it -> PUT /fin/jobs/{job_id} submits a result -> Execute()
returns the submitted variables to the (simulated) step machinery.
This is the automated regression-test counterpart of the manual
register -> trigger -> poll -> result -> report smoke test run earlier
against a live binary with examples/fin-playbook.json.
Existing coverage this builds on was already solid going in (queue
98%, fin capability 92.6%, api/fin 73.5%, finmemory 100%) - this adds
the one thing that was previously only verified manually: the full
stack working together end-to-end, not just each layer in isolation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- DELETE /fin/ (fin-token gated) now unregisters the calling fin itself -
the fin_id is inferred from the token, so it's never sent in the path
and there's no ownership check to get wrong.
- DELETE /fin/{fin_id} is repurposed as an admin/dashboard action (same
auth as GET /fin/ and GET /fin/{fin_id}) to forcibly remove any fin's
registration, e.g. one that's stale/offline and will never unregister
itself.
- Updated fin-protocol.md and route/handler doc comments accordingly.
- go.mod/go.sum: incidental `go mod tidy` cleanup (drops an unused
testcontainers-go dependency added but never wired up in an earlier,
since-abandoned exploration).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously a step routed to the Fin fallback capability always waited
out its full step timeout (or a 1-minute default) even when no Fin had
ever registered for its capability type, or every registered Fin had
gone stale. Both cases are knowable up front, so there is no reason to
wait:
- fincapability.Capability gains an IFinRepository dependency; before
enqueuing a job it checks whether any registered Fin declares the
step's capability type, and whether at least one of those is "live"
(LastSeen within a staleAfter threshold, default 2x
FIN_LONG_POLL_TIMEOUT_SECONDS). If not, it returns
fin.ErrNoCapableFin / fin.ErrOnlyStaleCapableFins immediately instead
of enqueuing. A nil repository (or a failing List() call) fails open
to the previous wait-based behaviour, so this is purely additive.
- fin.Record gains a computed, non-persisted Stale bool, populated by
GET /fin/ and GET /fin/{fin_id} using the same staleness threshold,
so GUIs/operators can flag Fins that are registered but likely no
longer polling without SOARCA needing to actively expire them.
Wired into internal/controller/controller.go: NewDecomposer passes
controller.finRepo/soarcaTime/staleAfter into fincapability.New, and
newFinHandler computes the matching StaleAfterSeconds for
fin_handler.Config so both checks agree on the same threshold.
Swagger docs regenerated for the new Stale field.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The LastSeen doc comment and §2.4 of FIN-WEBHOOK-PROTOCOL-PROPOSAL.md
still described the original proposal (stale Fins auto-expired from the
routing table via TTL), which is not what got implemented: registrations
are never removed automatically. Update both to describe what's actually
in place - stale Fins are flagged (Record.Stale / GET /fin[/{fin_id}])
for operator visibility, and fincapability.Capability's fail-fast check
uses the same staleness threshold to refuse new jobs when no live Fin
could possibly claim them, rather than an automatic reaper.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
fin.ErrRegistrationTokenInvalid has existed since the Fin protocol's data models were first defined but was never actually constructed anywhere - Register() used a plain string literal instead. Construct and return it now, matching the pattern used for the fin_token/job-related sentinel errors elsewhere in this package (ErrFinTokenInvalid, ErrJobNotFound, etc.), and use its Error() text for the API response instead of duplicating the message as a separate literal. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The unregister/delete split (DELETE /fin/ vs DELETE /fin/{fin_id}) and the
LastSeen staleness doc-text fix had updated Go source annotations but
swagger.json/yaml were never regenerated with make swagger-docs. Sweep
those up now. Also includes the command_b64 field being added to
fin.Command in the following commit (make swagger-docs was run once,
covering both).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
cacao.Command.CommandB64 (used by the built-in powershell capability as a base64 alternative to Command for complex/quoted scripts) was silently dropped when building fin.Command for a Job - a Fin-based capability relying on command_b64 would receive nothing. Add the field to fin.Command and forward it in toFinCommands. Also fix a stale doc comment that claimed Type/Content/Headers were dropped when the struct already carried them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t poll Previously only /poll updated a Fin's LastSeen. A Fin with limited concurrency running a long job (extending its lease via periodic status pings per §2.5, not polling again until it's free) would go stale by that measure alone - wrongly triggering fin.Capability's fail-fast rejection of new jobs for its capability type (ErrOnlyStaleCapableFins) and showing as stale in List/Get, even though it is actively alive and working. SubmitResult and StatusPing now also call repository.Touch(), tolerating failure the same way Poll already does (log and continue - liveness bookkeeping must never block the Fin-facing response). Updated LastSeen's doc comment on fin.Record to describe all three touch points. Regenerated swagger docs for the comment change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Manual and Fin are the only capabilities that enforce cacao.Step.Timeout as a hard wall-clock deadline (SSH/HTTP/OpenC2/PowerShell don't enforce it at all today - a separate, pre-existing gap, out of scope here). Both independently hardcoded a 1-minute fallback for when a step omits timeout entirely. 1 minute is unrealistically short as a deadline for what these two capabilities actually do: a human-in-the-loop Manual approval, or a Fin job that may involve real external work (e.g. multi-target SSH). The CACAO spec does not mandate this fallback value - its __ACTION_TIMEOUT__ constant (60000ms/1 minute) is merely a default value playbook authors may optionally assign to a step's own timeout, not a mandated platform-level fallback for when the property is omitted altogether; the spec only says implementations SHOULD consider implementing a maximum allowed timeout, without prescribing a number. We considered treating timeout as an inactivity/liveness signal instead (resettable by Fin status pings) given the spec's "used to determine when a step is no longer responsive" wording, but settled on keeping it a simple hard deadline - simpler to implement/reason about, and matches precedent (e.g. GitLab CI job timeouts). This keeps StatusPing's existing role unchanged (queue-side claim lease only, not the overall wait). Added pkg/utils.DefaultStepTimeout(), reading the new DEFAULT_STEP_TIMEOUT_SECONDS env var (default 600s/10 minutes), and switched both manual.go and fin_capability.go to call it instead of their own separate hardcoded constants - one shared, configurable default instead of two independent ones. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
hidde-jan
marked this pull request as draft
August 30, 2026 07:46
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An experiment with a HTTP-pull based fin protocol.
External fin worker register, poll for jobs, then report status updates and job results, all over HTTP.
No external broker necessary. Simplified messages mostly based on existing CACAO.
This is not meant as a (direct PR) just as a discussion piece.