You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This proposal addresses several raised issues and sets up long-term integration of GitProxy with the codified controls ecosystem. It updates how the proxy handles workflows around user actions — by moving to configurable workflows with explicit states and transitions. The goal is to add flexibility for organization-specific policies, workflows, and integrations, while still providing consistent handling for checks, scans, approvals, and routing decisions. This design can be delivered in smaller, incremental pieces rather than as an all-or-nothing rewrite; it describes the target redesign and serves as an ideal benchmark for future delivery and migration decisions.
As a potential, such a system could form it's own re-usable library - where codified policies, controls, evidence of execution + results are required as part of a system flow.
Requirements (Short)
Integration with common policy specifications (OSCAL), for traceability between policies and implemented controls.
Compatibility with open standards (Open Policy Agent), to preserve separation of concerns between control, policy, and workflow.
Flexibility to change the core workflow to meet organizational policies.
Implement organization-specific controls and scanners.
Clean separation of evidence handlers vs control handlers, with explicit declaration of control input requirements (evidence keys).
Non-functional
Support horizontal scaling, with the caveat that shared repository storage must be provided by the deploying organization (e.g., NFS or shared volume).
OSCAL integration: Support expressing controls and evidence mapping in OSCAL so policy intent and implementation remain traceable in audits and compliance reporting. Rationale: without a standards-based control model, the workflow becomes a one-off implementation that is hard to justify, certify, or map to external requirements.
OPA compatibility: Keep policy decisions externalized to OPA with a clear input/output contract. Rationale: policy changes should not require workflow code changes, and OPA provides a mature PDP with tooling, versioning, and safe rollout patterns.
Workflow flexibility: Allow states, handlers, and transitions to be altered per deployment. Rationale: organizations have different risk tolerances and gates; a fixed workflow forces forks or patching core logic.
Org-specific integrations: Enable custom steps such as internal ticketing, approvals, or notifications. Rationale: real-world governance flows depend on org systems, and the proxy must plug into them without bespoke forks.
Org-specific controls/scanners: Support adding or swapping scanners and controls per organization. Rationale: tooling preferences and regulatory obligations vary, so the platform must accept custom controls without re-architecting the core.
Evidence/control separation: Keep evidence production distinct from control evaluation, with explicit evidence keys as inputs. Rationale: this removes implicit dependencies, enables reuse, and makes decisions explainable and auditable.
Horizontal scaling: Run multiple proxy instances with shared storage and transactional state. Rationale: production deployments require concurrency and resilience, which demands a stateless runtime with shared persistence.
Notes
OSCAL (control framework language + implementation/evidence mapping as compliance artifacts)
Below is a brief critique of the current chain-based workflow in the codebase, based on the existing handler ordering and data dependencies.
The push pipeline is a single linear chain with implicit dependencies between handlers (see src/proxy/chain.ts). There is no explicit contract that declares which handler produces which data, so later handlers silently depend on earlier side effects.
Data dependencies are implicit and spread across mutable action fields. Examples:
Evidence gathering and control logic are intertwined inside the chain rather than being separated as distinct handler types. This makes it hard to reuse evidence across controls or to swap control logic without also changing evidence order.
The chain mixes parsing, auth, repo cloning, scanning, and gating concerns in one ordered list. This makes workflow changes brittle because ordering is the only coordination mechanism (see src/proxy/chain.ts).
Push and pull are modeled inconsistently: push uses a dedicated chain with side-effectful steps (clone, diff, scan, write-pack), while pull paths apply a different set of checks and short-circuit logic, so there is no shared contract for evidence, control evaluation, or decision outcomes across actions.
Push exposes an explicit wait/approval path (via checkIfWaitingAuth/blockForAuth), but pull does not use an equivalent pending/approval mechanism, which means similar policy requirements must be implemented twice with different semantics.
How This Proposal Solves Current Issues (and Linked Issues)
Explicit evidence/control separation removes implicit data coupling in the chain by declaring evidence types and control inputs, which directly addresses the critique about hidden dependencies.
Declarative state definitions replace fragile ordering with explicit transitions, making workflow changes safer and aligning with #1350 and #1365.
Shared push/pull model applies the same evidence/control contract, decision statuses, and pending flow to both actions, removing the current divergence between push chains and pull short-circuit checks.
Re-entrant pending flow replaces ad-hoc auth blocks with a consistent pending path, which supports review workflows and notification integrations like #1121.
OPA-based policy control centralizes decision logic (control vs policy separation) in support of #99.
Plugin-based handlers allow organization-specific scanners and controls (aligned with #26).
Workflow Engine vs State Machine
The current code already behaves similarly to a very simple state machine (a mostly linear chain with early exits). Moving to a full workflow engine would likely add complexity without solving a clear problem.
The simplest and most maintainable approach is:
Use a state machine per action (Push, Pull, future actions).
Keep a declarative workflow definition (JSON/YAML) describing states, transitions, and handlers.
Implement a lightweight workflow runner that executes handlers, applies transitions, and emits events/audit records.
Overview
Core concepts:
Action: a single user request type (push | pull | …).
State Flow: config per action (states, handlers, transitions).
State: named phase of execution (VALIDATING, ANALYZING, DECIDING, …).
Evidence handlers: gather facts and produce evidence.
Control handlers: evaluate controls using evidence and return decisions.
Result: allow/block/error (derived from control handler results and transitions).
Plugin integration: plugins are externally registered handlers (and optionally guards/events) without patching core logic/config.
Basic Hierarchy
Action
└── State
├── EvidenceHandler
└── ControlHandler
Handler Execution Model
States contain ordered evidence handlers followed by ordered control handlers. The runner will:
Enters a state (records state-enter).
Executes evidence handlers in order, storing evidence by (actionId, evidenceKey) and recording their results.
Executes control handlers in order, each reading required evidence keys and returning pass/fail/pending/error.
Aggregates control results into a state outcome (pass/fail/pending/error).
Selects the next transition based on the outcome.
Records state-exit, then transitions.
Handler Types
Handlers are split into two types with different semantics:
Evidence handlers: gather facts and produce named evidence (diff, commit messages, author emails, scan outputs). They return pass | error and do not directly decide pass/fail.
Control handlers: evaluate controls and return pass | fail | pending | error. These drive state transitions.
Evidence and Control Dependencies
Evidence and control handlers are decoupled. Evidence handlers declare the evidence types they produce. Control handlers declare the evidence types they require. The workflow runner wires them together by matching evidence types.
An evidence_gather handler is the canonical pattern for collecting a specific evidence type. Each evidence type is identified by a stable key (for example: evidence.git.diff, evidence.git.commit_messages). Control handlers depend on those stable keys, not on specific evidence handler implementations. This keeps evidence gathering interchangeable and allows multiple implementations to produce the same evidence type.
Handlers may return pending to pause the workflow in the current state.
Re-entry resumes from persisted Action state, not in-memory runtime state.
All handlers must be idempotent (safe to run multiple times).
Async and Re-Entrant Handlers
Some handlers are long-running or require external input (e.g. require_approval). The workflow must support pausing and resuming, this is handled through the 'pending' state
UI-Driven Approvals - (A type of re-entrant handler)
Some decisions are manual - These decisions are recorded via the GitProxy UI which can pass / fail the state of a specific handler.
Model
Re-entrant handlers (e.g. require_approval), when that handler runs, it ensures an approval record exists (or creates one) and returns pending.
The UI reads pending approval records and allows reviewers to:
Pass → update the handler result to pass
Fail → update the handler result to fail
Cancel → update the handler result to fail with an additional reason that it was cancelled
Cancellation semantics
'Cancel' is operationally different from 'reject', but the state machine needs a finite set of statuses. The simplest option is:
Treat cancelled as fail with failedReason = "cancelled"
How the workflow resumes
UI updates the status of a specific handler.
Event trigger: Engine locates the persisted handler-state, and updates the status field pass/fail. The handler will automatically trigger an onChange event, causing the state-machine to recalculate and perform necessary next steps.
Stateless and Idempotent Design
Multiple proxy instances may be running concurrently as part of the horizontal scaling requirement. Therefore:
Application code must be stateless, outside of an operation currently in flight (which should be protected with optimistic locking).
The authoritative source of state is a transactional store.
Runtime must rehydrate from storage and continue deterministically.
A locking mechanism should be thought through - ideally if the database handles locking, the record would be locked from the read to the update with optimistic locking or similar.
Idempotency expectations:
If a handler is re-run, it must detect prior completion and return the same result (or a compatible result), without duplicating side effects.
Side effects must be guarded with stable keys (e.g. “approval_request_id”, “scan_run_id”) stored in HandlerResult.data.
If a handler is in an Error state, the handler can be re-run in it's entirety.
Workflow Definition Sketch
NOTE:I think it should be possible to remove evidence_handlers and declare only control handlers, as control handlers know which evidence is required, therefore which evidence handlers can provide such evidence
User defined plugin handlers: plugin.<vendor>.<handler> (namespaced, stable).
Extensibility: Events
Three extension approaches:
** core Structural changes and extension**
Add / customize states/handlers in the main YAML file.
This allows for maximum flexibility, but has the downside of being quite a core structural change, where the maintainer of the deployed instance becomes responsible for the workflow and each action and ensuring mandatory handlers are synced with new updates to git-proxy.
** Register a handler specific to your deployment in the deployment**
# index.ts// registry your custom handlerregistry.register('plugin.security.gitleaks',myextensions.gitleaksScan);// Apply it to a stateregistry.addHandlerToState('PUSH','ANALYZING','plugin.security.gitleaks');
This allows for a fully defined additional handler to be inserted into a specific state for an action. This allows for a fully fledged handler to be implemented, but still quite 'heavy'as it requires the full handler implmentation, with exit states etc. to be implemented.
Event-based extension
Observe lifecycle events without modifying the workflow
Event hooks (concept):
state:enter, state:exit
handler:before, handler:after, handler:error
Example: Send email on Pull completion
workflow.on('state:exit',({ actionId, actionType, state })=>{if(actionType==='pull'&&state==='COMPLETED'){email.send({to: getUserEmail(actionId),subject: 'Next steps: how to push',body: buildPushInstructions(actionId),});}});
This is the easiest and simplest way to extend - workflow.on allows simple code to be hooked in on events, the workflow.on DOES not modify status - A very simple hook to run some extra non-state-chaning code. Great for cross-cutting concerns (audit, metrics), or small non-state-changing extensions (sending emails, notifications etc)
OPA does not run workflows; it returns a decision + reasons + obligations. Your workflow runner enforces that decision by mapping to handler status pass/fail/pending (and possibly creating approvals).
This section adds a coherent architecture for expressing policies as OSCAL controls (control framework language) and mapping them to implementations in GitProxy (handlers/plugins) and runtime evaluation in OPA.
This section makes the OSCAL integration “real”: it defines the supply chain that turns OSCAL artifacts into runtime policy and data that OPA can evaluate with low latency.
flowchart LR
A["Git Request\n(push/pull)"] --> B["Workflow Runner\n(State Machine)"]
B --> C["Handlers\nVALIDATING/ANALYZING\n(evidence)"]
C --> D["HandlerResult Store\n(transactional)"]
B --> E["evaluate_policy\n(OPA client)"]
E --> F["OPA Agent\n(PDP)"]
F --> G["OPA Bundle\n(rego + data.json)\ncompiled from OSCAL"]
F --> H["Decision + Obligations\nallow/deny/pending"]
H --> I["require_approval\n(UI-driven re-entrant handler)"]
I --> J["Approval UI"]
H --> K["Transition\nAPPROVED/REJECTED/PENDING"]
K --> L["Persist Action + StateHistory + Audit"]
L --> M["Respond to Git\nallow/block"]
Loading
C4. OPA Agents (deployment patterns)
Sidecar (recommended for k8s)
OPA runs next to GitProxy (same pod)
low latency, independent scaling/rollout of policy bundles
Central PDP
one OPA cluster serves many GitProxy instances
centralized governance and caching
needs HA + scaling + tenancy controls
Embedded WASM
compile policies to WASM and embed in GitProxy
fastest decision path
policy updates become a “hot-load WASM module” problem
Appendix D: OPA Policies (Rego DSL) — Examples
Package: gitproxy.authz
Rule: decision
API path: /v1/data/gitproxy/authz/decision
future Architecture goals and high-level design
This proposal addresses several raised issues and sets up long-term integration of GitProxy with the codified controls ecosystem. It updates how the proxy handles workflows around user actions — by moving to configurable workflows with explicit states and transitions. The goal is to add flexibility for organization-specific policies, workflows, and integrations, while still providing consistent handling for checks, scans, approvals, and routing decisions. This design can be delivered in smaller, incremental pieces rather than as an all-or-nothing rewrite; it describes the target redesign and serves as an ideal benchmark for future delivery and migration decisions.
As a potential, such a system could form it's own re-usable library - where codified policies, controls, evidence of execution + results are required as part of a system flow.
Requirements (Short)
Non-functional
Issue Map (Requirements -> Issues)
Requirement Details and Rationale
Notes
embedded)
Critique of Current Workflow (Chain) Design
Below is a brief critique of the current chain-based workflow in the codebase, based on the existing handler ordering and data dependencies.
actionfields. Examples:getDiffdepends oncommitDataand commit range fromparsePush, and on a populatedproxyGitPathfrompullRemote(src/proxy/processors/push-action/parsePush.ts, src/proxy/processors/push-action/pullRemote.ts, src/proxy/processors/push-action/getDiff.ts).scanDiffexpects a priordiffstep to have stored content in the action steps list (src/proxy/processors/push-action/scanDiff.ts).writePackassumesproxyGitPathandrepoNameare set by earlier steps (src/proxy/processors/push-action/writePack.ts).gitleaksassumescommitFrom/commitToand repository clone state are already set up (src/proxy/processors/push-action/gitleaks.ts).checkIfWaitingAuth,blockForAuth) rather than a unified pending/approval mechanism. This makes re-entry behavior implicit and difficult to reason about (src/proxy/processors/push-action/checkIfWaitingAuth.ts, src/proxy/processors/push-action/blockForAuth.ts).checkIfWaitingAuth/blockForAuth), but pull does not use an equivalent pending/approval mechanism, which means similar policy requirements must be implemented twice with different semantics.How This Proposal Solves Current Issues (and Linked Issues)
pendingpath, which supports review workflows and notification integrations like #1121.Workflow Engine vs State Machine
The current code already behaves similarly to a very simple state machine (a mostly linear chain with early exits). Moving to a full workflow engine would likely add complexity without solving a clear problem.
The simplest and most maintainable approach is:
Overview
Core concepts:
Basic Hierarchy
Handler Execution Model
States contain ordered evidence handlers followed by ordered control handlers. The runner will:
(actionId, evidenceKey)and recording their results.Handler Types
Handlers are split into two types with different semantics:
Evidence and Control Dependencies
Evidence and control handlers are decoupled. Evidence handlers declare the evidence types they produce. Control handlers declare the evidence types they require. The workflow runner wires them together by matching evidence types.
An
evidence_gatherhandler is the canonical pattern for collecting a specific evidence type. Each evidence type is identified by a stable key (for example:evidence.git.diff,evidence.git.commit_messages). Control handlers depend on those stable keys, not on specific evidence handler implementations. This keeps evidence gathering interchangeable and allows multiple implementations to produce the same evidence type.Evidence Handler Result
status: pass | errorevidenceTypes: list of evidence types produceddata: evidence payload (diff summary, scan report, metadata)Control Handler Result
status: pass | fail | pending | errorrequiredEvidence: list of evidence types neededdata: decision metadata (reasons, required approvals, references)Requirements
pendingto pause the workflow in the current state.Async and Re-Entrant Handlers
Some handlers are long-running or require external input (e.g.
require_approval). The workflow must support pausing and resuming, this is handled through the 'pending' stateUI-Driven Approvals - (A type of re-entrant handler)
Some decisions are manual - These decisions are recorded via the GitProxy UI which can pass / fail the state of a specific handler.
Model
require_approval), when that handler runs, it ensures an approval record exists (or creates one) and returnspending.passfailfailwith an additional reason that it was cancelledCancellation semantics
'Cancel' is operationally different from 'reject', but the state machine needs a finite set of statuses. The simplest option is:
failwithfailedReason = "cancelled"How the workflow resumes
UI updates the status of a specific handler.
Stateless and Idempotent Design
Multiple proxy instances may be running concurrently as part of the horizontal scaling requirement. Therefore:
Idempotency expectations:
HandlerResult.data.Workflow Definition Sketch
NOTE: I think it should be possible to remove evidence_handlers and declare only control handlers, as control handlers know which evidence is required, therefore which evidence handlers can provide such evidence
Logical Storage Model
Action
StateHistory
HandlerResult
Evidence
Mapping Handler Names to Implementation (Registry)
Workflow config references stable handler keys. Code binds keys to concrete implementations via a registry.
Design goals:
Handler Function Shape
Registry Interface
Implementation Sketch (with aliases + validation)
Core + Plugin Registration Example
Adding a plugin at runtime.
Recommended conventions:
snake_case(e.g.,check_repo_allowlist).plugin.<vendor>.<handler>(namespaced, stable).Extensibility: Events
Three extension approaches:
** core Structural changes and extension**
This allows for maximum flexibility, but has the downside of being quite a core structural change, where the maintainer of the deployed instance becomes responsible for the workflow and each action and ensuring mandatory handlers are synced with new updates to git-proxy.
** Register a handler specific to your deployment in the deployment**
This allows for a fully defined additional handler to be inserted into a specific state for an action. This allows for a fully fledged handler to be implemented, but still quite 'heavy'as it requires the full handler implmentation, with exit states etc. to be implemented.
Event-based extension
Event hooks (concept):
state:enter,state:exithandler:before,handler:after,handler:errorExample: Send email on Pull completion
This is the easiest and simplest way to extend - workflow.on allows simple code to be hooked in on events, the workflow.on DOES not modify status - A very simple hook to run some extra non-state-chaning code. Great for cross-cutting concerns (audit, metrics), or small non-state-changing extensions (sending emails, notifications etc)
Appendix A: OPA Integration (Runtime Policy Evaluation)
OPA is the decision engine for “is this allowed?” based on context:
OPA does not run workflows; it returns a decision + reasons + obligations. Your workflow runner enforces that decision by mapping to handler status pass/fail/pending (and possibly creating approvals).
A2. OPA Decision Contract
Input (what
evaluate_policysends){ "request_id": "act_123", "action": "push", "repo": { "full_name": "org/repo", "visibility": "private", "default_branch": "main", "tags": ["prod", "pci"] }, "ref": { "type": "branch", "name": "main" }, "actor": { "username": "grovesy", "groups": ["devs", "platform"], "roles": ["developer"], "mfa": true }, "context": { "workflow_state": "DECIDING", "workflow_version": "2026-02-09", "ip": "203.0.113.10" }, "evidence": { "scans": { "gitleaks": { "status": "completed", "findings": [ { "rule": "aws-access-key", "severity": "high", "file": "secrets.txt" } ] } }, "risk": { "score": 8.4, "reasons": ["secret_high"] } }, "approvals": { "security": { "status": "none" }, "repo_admin": { "status": "none" } } }Output (what OPA returns)
{ "decision": "pending", "reasons": [ "High severity secret detected on protected branch main", "Security approval required when risk.score >= 7" ], "obligations": [ { "type": "require_approval", "key": "security", "min_reviewers": 1 } ], "policy": { "package": "gitproxy.authz", "rule": "decision", "version": "bundle:prod@sha256:abcd..." } }Mapping to workflow:
allow→ handler resultpassdeny→ handler resultfailpending→ handler resultpending(+ obligations forrequire_approval)Appendix B: OSCAL Integration (Controls + Implementations + Evidence)
This section adds a coherent architecture for expressing policies as OSCAL controls (control framework language) and mapping them to implementations in GitProxy (handlers/plugins) and runtime evaluation in OPA.
B1. Policy Model: OSCAL → OPA → Workflow
B2. Where OSCAL touches the state machine
The state machine remains unchanged structurally:
evaluate_policy(calls OPA with runtime input + control_set reference)require_approval(enforces obligations returned by OPA)The only addition is that
evaluate_policyincludes acontrol_setpointer in the OPA input so decisions can vary by environment/profile.Example addition to OPA input
{ "control_set": { "oscal_profile_id": "gp-profile-prod", "version": "2026-02-09" } }B3. OSCAL artifact usage
Use these OSCAL models together:
B4. Example OSCAL control + implementation intent (illustrative)
{ "control": { "id": "gp-secret-detection", "title": "Secrets must not be pushed to protected branches", "description": "High severity secrets require approval", "params": [ { "id": "severity-threshold", "value": "high" }, { "id": "protected-branch-mode", "value": "default_branch_only" } ], "implementation": { "enforced-by": ["plugin.security.gitleaks", "evaluate_policy", "require_approval"], "evidence": [ { "type": "scan", "source": "gitleaks", "fields": ["severity", "rule", "file"] } ], "obligations": [ { "type": "approval", "key": "security", "when": "severity >= high AND protected_branch = true" } ] } } }B5. Mapping controls to handlers/plugins
A control maps to enforcement and evidence producers:
gp-secret-detectionplugin.security.gitleaksevaluate_policy,require_approvalgp-mfa-protected-branchparse_request, identity contextevaluate_policygp-risk-gateplugin.risk.scoreevaluate_policy,require_approvalAppendix C: Reference Architecture (OSCAL → OPA Bundles) and OPA Agents
This section makes the OSCAL integration “real”: it defines the supply chain that turns OSCAL artifacts into runtime policy and data that OPA can evaluate with low latency.
C1. Reference Architecture Diagram (Policy Supply Chain)
flowchart TD subgraph Authoring["Policy Authoring (Git)"] CAT["OSCAL Catalog"] PROF["OSCAL Profile (Tailoring/Params)"] COMP["OSCAL Component Definition (Implementation Mapping)"] MAPT["Mapping Templates (OSCAL→Policy Model)"] end subgraph Pipeline["Policy Build Pipeline (CI)"] VAL["Validate OSCAL + Schemas"] NORM["Resolve Profile + Normalize Controls"] GEN["Generate Runtime Control Data"] REGO["Generate/Assemble Rego Modules"] BLD["Build OPA Bundle (rego + data.json)"] SIGN["Sign/Attest Bundle (optional)"] end subgraph Distribution["Distribution"] REG["Bundle Registry (OCI/HTTP/S3)"] end subgraph Runtime["Runtime"] OPAAG["OPA Agent (sidecar/central/embedded)"] GP["GitProxy Workflow Engine"] end CAT --> VAL PROF --> VAL COMP --> VAL MAPT --> GEN VAL --> NORM --> GEN --> REGO --> BLD --> SIGN --> REG REG --> OPAAG GP -->|evaluate_policy| OPAAGC2. What gets generated from OSCAL
A practical split is:
Example
data.jsonshape:{ "control_sets": { "gp-profile-prod": { "version": "2026-02-09", "controls": { "gp-secret-detection": { "enabled": true, "params": { "severity_threshold": "high" }, "applies_to": { "protected_branches": true }, "obligations": [ { "type": "require_approval", "key": "security", "min_reviewers": 1 } ] } } } } }Rego then consults
data.control_sets[input.control_set.oscal_profile_id]...to apply the correct control set.C3. Unified runtime diagram (GP + workflow + OSCAL + OPA)
C4. OPA Agents (deployment patterns)
Sidecar (recommended for k8s)
Central PDP
Embedded WASM
Appendix D: OPA Policies (Rego DSL) — Examples
Package:
gitproxy.authzRule:
decisionAPI path:
/v1/data/gitproxy/authz/decisionD1. Base skeleton
D2. Deny protected-branch push without MFA
D3. Secret finding triggers approval obligation
has_high_secret { some i f := input.evidence.scans.gitleaks.findings[i] f.severity == "high" } decision := out { is_push protected_branch has_high_secret input.approvals.security.status != "approved" out := { "decision": "pending", "reasons": ["High severity secret detected; security approval required"], "obligations": [ {"type": "require_approval", "key": "security", "min_reviewers": 1} ] } }Appendix E: Operational Notes (Auditability + Versioning)
For every request/action, persist:
This enables: