picatz / picatz/flowstate

call: as the composition keystone — the cohesive vision, and the sins it must not inherit

Open
#172 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

design dsl kind/design-record
Dominant language
Go
Stars
9
Forks
0
Avg merge
3h 3m
Merged PRs (30d)
509

Description

PR #168 lands the mechanism. This issue records the whole shape call: is growing into — reuse across teams, remote sources, identity, observability, LSP — so each future slice lands against a stated vision instead of accreting. A focused research pass on how composition went wrong in GHA reusable workflows, Terraform modules, CircleCI orbs, and GitLab includes is running and will be reconciled into this issue; what follows is the position to check that research against.

The load-bearing decision: composition is vendoring, not linking

call: resolves at compile time and embeds the callee whole, digest-recorded. This is the single choice most of GHA's composition sins hang on, so it's worth stating why it's right rather than treating it as an implementation detail:

  • A runtime-resolved reference (uses: org/repo@v1) means the thing that runs is decided later than the review. Tag-moving attacks are just the malicious face of it; the benign face — a run meaning something different on Tuesday than it meant in Monday's review — is equally disqualifying for durable workflows, and invariant 10 already forbids it independently.
  • Compile-time embedding means the calling author had the callee's exact bytes in front of them when they shipped. The trust boundary is at authoring, where a human (or agent) can actually review, not at run time, where nobody is looking. source_digest makes the provenance checkable after the fact.

Consequence for remote workflows: remote sources are a compiler concern, never an engine concern. flow (CLI/LSP) may learn to fetch call: <remote-source> at compile time — with a digest pin required, no floating references, and Terraform's lockfile lessons applied — but the engine never fetches anything: a submitted spec is hermetic, forever. Local-only is the correct requirement for the engine permanently; remote is sugar at the edge. A registry, if ever, is a distribution mechanism for the same digest-pinned bytes, not a new trust model.

Identity at the boundary — the GHA-shaped question, answered precisely

The instinct that this rhymes with GHA's identity problems is right, and the answer needs to be principled rather than incidental. The shape: a callee runs inside the caller's run, under the run's tenant and identity. A callee that names ${secret('env:X')} resolves it with the caller's authority — structurally similar to GHA's secrets: inherit, which is on the sin list. Why it is not the same sin, and where the real line is:

  1. What's already refused: the caller cannot push a secret across with: (compile-time refusal, positioned diagnostic). So authority never flows in silently from the caller's side.
  2. What's deliberately allowed: a callee's own spelled-out secret references resolve under the run's authority — because the callee is vendored code the author reviewed, exactly as an imported Go package that reads a credential is the importer's responsibility. secrets: inherit's sin was runtime-resolved third-party code receiving ambient secrets; vendored, digest-pinned, reviewed bytes receiving explicitly-spelled ones is the trust model working as designed.
  3. What defense in depth adds anyway: the existing per-tenant secret policy rules (auth secret rules, CEL, fail-closed) already bound what any run — composed or not — may resolve; that is the operator's independent control, and it holds regardless of composition depth. If real multi-team usage shows a need for finer grain (a caller constraining a callee harder than the tenant policy does), the candidate is an allow_secrets: narrowing on the call step — recorded here per the #166 discipline: named, not scheduled, no new surface before a demonstrated need.
  4. Never: an ambient token, an implicit permissions inheritance, or any authority the file doesn't spell.

Observability: the call tree is never flattened

GHA flattens reusable-workflow logs into the caller and operators hate it; Terraform's module paths get addressing right. The rule here: every surface that names a step names the path — progress, pending activities, spans, flow get, error text (workflow "provision": step "deploy": ... already does this). A call step's span nests the callee's spans; flow watch shows the nested structure because the schema carries it, not because the TUI parses names. As call trees deepen, path addressing (caller.callee.step) becomes the one spelling everywhere — logs, filters, diagnostics alike.

LSP and diagnostics: the signature is the ergonomics

The callee's inputs:/outputs: are the contract, so the editor experience should feel like calling a typed function: hover on a call step shows the callee's declared signature and docs; a with: argument of the wrong type is a diagnostic positioned at the argument in the caller's file; a diagnostic inside the callee (when editing it) never phantom-appears in the caller. Terraform's LSP art shows the failure modes to refuse: stale module caches producing phantom errors, and cross-file resolution doing I/O on the keystroke path. The existing rule covers the second — the LSP is path-aware only when a document URI names a real file, and remote fetch (when it exists) is an explicit compile action, never keystroke-triggered.

The footgun ledger (each already held, each kept deliberately)

Depth bounded at execution not just parse (hand-built specs); breadth bounded by CheckSpecSize post-expansion; isolation as the default with zero ambient scope; present-empty outputs so "ran with nothing to say" ≠ "skipped"; suspension transparent so a callee's durable wait is not a surprise; a tolerated callee failure spelled identically on both drivers. Any future slice that would weaken one of these argues against a written reason.

Sequencing: reconcile the research report here; then the reuse story's next real slice is the #165 slice-2 example (a library workflow with two callers) which will pressure-test signature ergonomics before any remote-source work is considered.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reconcile the research on GHA reusable workflows, Terraform modules, CircleCI orbs, and GitLab includes into issue #172. Review PR #168, the CheckSpecSize behavior, and the flow get/flow watch surfaces, then use the planned #165 slice-2 library workflow with two callers to pressure-test the signature ergonomics. Done means the research is reconciled and the next slice has explicit conclusions.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
cli, devtools
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.