Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Agent Architecture

The Agent is the central component that connects CI/CD job lifecycle with runtime events.

One agent process runs on one host and can observe multiple CI/CD jobs at the same time. Kernel-side observation is handled with eBPF. Job lifecycle and rule evaluation are handled in userspace.

Architecture

flowchart TB
    START["host start / project start"]
    PROXY["dockerd proxy"]

    subgraph A["Agent"]
        direction TB
        L["Listener<br/>(Unix socket)"]

        subgraph JR["JobRegistry"]
            direction TB
            subgraph JOBS["Jobs"]
                direction TB
                EVAL["Evaluation"]
                SCOPE["Scope<br/>host / project"]
            end
        end

        subgraph KR["Kernel Runtime"]
            direction TB
            KT["KernelTracker<br/>Job state management<br/>(cgroup / process tracking)"]
            EBPF["eBPF Runtime"]
            KIO["KernelIO"]
            KT --> EBPF
            EBPF -->|"map operations"| KIO
            KIO -->|"decoded samples"| KT
        end

        subgraph OUT["Outputs"]
            direction TB
            LOGS["Job logs"]
            RESULT["Project result"]
        end
    end

    K["Kernel"]

    START --> L
    PROXY --> L
    L --> JOBS
    KT -->|"EventRecord"| EVAL
    EVAL --> SCOPE
    JR -->|"tracking commands"| KT
    KIO <-->|"eBPF maps / ringbuf"| K
    SCOPE --> LOGS
    SCOPE --> RESULT

    classDef agentOuter fill:transparent,stroke:#0f766e,color:#064e3b,stroke-width:2px;
    classDef registry   fill:#d1fae5,stroke:#0f766e,color:#064e3b,stroke-width:1px;
    classDef kernel     fill:#ccfbf1,stroke:#0f766e,color:#134e4a,stroke-width:1px;
    classDef outputs    fill:#dcfce7,stroke:#0f766e,color:#14532d,stroke-width:1px;
    classDef leaf       fill:#ffffff,stroke:#94a3b8,color:#374151,stroke-width:1px;
    class A agentOuter
    class JR,JOBS registry
    class KR kernel
    class OUT outputs
    class L,EVAL,SCOPE,KT,EBPF,KIO,LOGS,RESULT leaf

This diagram is the reference point for reading the Agent implementation. host start, project start, and dockerd proxy staging requests enter the Agent through the Listener over a Unix socket. JobRegistry issues tracking commands to the Kernel Runtime. Scope owns rule, summary, and output state, but it does not operate the Kernel Runtime directly.

Concepts

Job

A Job is one CI/CD job tracked by the Agent. It is identified by the provider-supplied job identity (repository, workflow run, job name, runner) and owns its own cgroup tracking, rule evaluation, scope-local summaries, and outputs. The Agent can run many Jobs at the same time; each Job is finalized independently when its work completes.

The Agent separates job identity from job metadata. Identity is required to register and track a Job. Metadata is attached to the Job for logs, reports, and search.

CategoryFieldsRequiredPurpose
Job identityprovider, provider_host, project_pathYesCommon provider identity for every Job
GitHub identitygithub_run_id, github_job, github_run_attempt, github_runner_tracking_idYes for GitHub JobsIdentifies a GitHub Actions job run attempt and runner tracking ID
GitLab identitygitlab_job_idYes for GitLab JobsIdentifies a GitLab CI job execution
Job metadatacommit_sha, ref_name, trigger, actor_id, actor_name, github_workflow_ref, github_workflow_sha, github_workflow, gitlab_job_name, gitlab_config_ref_uriNoEnriches logs, reports, and triage

Scope

A Scope is the configuration / control surface attached to a Job. Two kinds exist, and a single Job may have one or both:

ScopeOwnerWhere it comes fromTypical setup
Host scopeHost operator (e.g., the platform team that installs cicd-sensor on the runner host)host start from a runner hookSelf-hosted runners, where the agent is provisioned by infrastructure
Project scopeProject / repository operator (e.g., the team owning the workflow)project start from the cicd-sensor-action (or equivalent)GitHub-hosted runners, where each workflow brings its own configuration through the Action

Each scope carries its own rules, evaluation state, and output destinations. The two are isolated: one scope cannot read or override the other’s rules, and their outputs are emitted separately. This lets the host operator and the repository operator each configure cicd-sensor for their own concerns without interfering with each other — the host operator can enforce a baseline across all jobs on the host, while a repository can layer project-specific rules on top.

For implementation ownership boundaries, see Agent Ownership Boundaries.

Subsystems

SubsystemResponsibility
AgentTop-level orchestrator for Listener, JobRegistry, KernelTracker, and shutdown
ListenerReceives start / staging requests over the Unix socket and handles provider routes and peer credentials
JobRegistryHandles job registration, host / project scope attachment, KernelTracker primitive composition, and finalize
Jobs / ScopeHandles per-job event workers, rule evaluation, and scope-local summaries / outputs
KernelTracker / eBPF RuntimeHandles cgroup / process tracking, kernel sample decoding, and EventRecord attribution
OutputsHolds runtime summaries used as inputs for job logs, project results, reports, and attestations

Provider Flow

ProviderRunnerStart entrypointcgroup seed
GitHub ActionsGitHub-hosted runner/v1/github/project/startcgroup of the project start peer PID
GitHub ActionsSelf-hosted Machine Runner/v1/github/host/startcgroup of the hook peer PID
GitLab CI/CDSelf-hosted Docker executor/v1/gitlab/staging/put -> lazy /v1/gitlab/host/startDocker label evidence and staging promote
GitHub ARC / GitLab Kubernetes executorPlannedTBDNRI / Pod metadata and similar options are under consideration

The agent process selects one provider at startup. The Listener mounts either /v1/github/* or /v1/gitlab/*, not both.

Listener trust model

A CI/CD job process typically has host-control-level permissions on the runner. Isolation between jobs comes from the layer below — a fresh VM per job for self-hosted runners, a fresh container for Kubernetes-based runners. cicd-sensor treats the runner host as a single trust domain inside that boundary.

The Listener’s per-request checks identify which Job (and which UID) the request belongs to, and keep each Job’s events and configuration from being attributed to another Job. They are not a strong access control.

The control socket is mode 0o777; request identification uses SO_PEERCRED:

CheckEndpointsWhat it confirms
Agent-owner UIDGitLab host/start, GitHub / GitLab staging/putpeer UID matches the agent process owner
Peer in tracked JobGitHub host/end, project/result, job/healthpeer PID’s cgroup is in an already-tracked Job
SeedGitHub host/startpeer’s cgroup becomes the new Job’s tracked root

GitHub project/start is the mixed case: on a self-hosted runner the peer must already belong to the host Job (it attaches project scope); on a hosted runner no prior Job exists, so the peer’s cgroup seeds a new project-only Job. Co-resident untrusted local users are out of scope.

KernelTracker Primitives

Job tracking is expressed by JobRegistry composing KernelTracker primitives.

PrimitiveMeaning
RegisterJob(jobID)Creates userspace job state and a per-job event channel. Does not touch BPF maps.
BindProcessCgroupToJob(jobID, pid)Resolves the PID cgroup and adds it to tracked_cgroups.
StageCgroupBasenameForJob(basename, jobID)Stages a Docker cgroup basename.
RemoveJob(jobID)Cleans up job state, cgroup bindings, staging entries, and process context.

RegisterJob and cgroup binding are separate operations. GitHub can resolve the cgroup from the peer PID at start time, so it uses RegisterJob + BindProcessCgroupToJob. GitLab Docker executor registers a job from label identity evidence and waits for a later staging promote.

Design Notes

  • Job membership is determined by cgroup tracking. Process context is a fat node snapshot with exec_path / argv / ancestors; it is not used as the job boundary.
  • KernelTracker state is owned exclusively by its loop goroutine. jobTrackingState is not published externally.
  • Listener stays as the delivery layer. Provider differences are contained in handlers and JobRegistry primitive composition.
  • Output runtime is scope-local. Host / project output queues are not hoisted into JobRegistry.
  • JobRegistry owns lifecycle. It is not on the hot path for event routing.

For kernel-side observation details, see eBPF Runtime. For Agent implementation ownership rules, see Agent Ownership Boundaries. For the rule authoring surface, see Rules. For the rule implementation, see Rule Engine.