Skip to content

Experiment: HTTP protocol for fins - #395

Draft
hidde-jan wants to merge 22 commits into
COSSAS:developmentfrom
hidde-jan:experiment/fin-http-protocol
Draft

Experiment: HTTP protocol for fins#395
hidde-jan wants to merge 22 commits into
COSSAS:developmentfrom
hidde-jan:experiment/fin-http-protocol

Conversation

@hidde-jan

@hidde-jan hidde-jan commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

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.

hidde-jan and others added 22 commits August 29, 2026 06:28
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
hidde-jan marked this pull request as draft August 30, 2026 07:46
@hidde-jan hidde-jan changed the title Experiment/fin http protocol Experiment: HTTP protocol for fins Aug 30, 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.

1 participant