Documentation
¶
Overview ¶
Package detection runs the detection job for an app and records the result.
It is the seam between detection (which knows about repositories and detectors) and the rest of Pando (which knows about apps, policy and audit). Sequence A's numbered steps 6 through 12 live in internal/detect; the parts that need an app row, the source allowlist and a place to write the answer live here.
Index ¶
- Constants
- Variables
- func Await(ctx context.Context, r DetectionReader, c clock.Clock, appID string, ...) (state.Detection, error)
- func DefaultConcurrency() int
- type AuditEvent
- type Auditor
- type DetectionReader
- type ImageBuilder
- type Installation
- type PortAllocator
- type Queue
- type RegistryInstallation
- type RegistryScreeners
- type Runner
- func (r *Runner) Built(ctx context.Context, appID string) (*detect.Built, error)
- func (r *Runner) Check(ctx context.Context, appID string) error
- func (r *Runner) Detect(ctx context.Context, appID string) (state.Detection, error)
- func (r *Runner) Rebuild(ctx context.Context, appID string) (bool, error)
- func (r *Runner) Revise(ctx context.Context, appID, message string) (state.Detection, error)
- func (r *Runner) RunQueued(ctx context.Context, appID string) (state.Detection, error)
- type ScreenPolicy
- type Screeners
- type SourcePolicy
- type SourceScanner
Constants ¶
const ActionRevise = "detection.revise"
ActionRevise is the audit event a revision writes (R-337): repository contents went to the provider because a person asked.
const ActionScreen = "detection.screen"
ActionScreen is R-337's event.
const MaxInstruction = 2_000
MaxInstruction is the longest message a person may send, in characters.
const MaxWait = 60 * time.Second
MaxWait is the longest a caller may ask Await to hold a request open.
Long enough that a client waiting out a trial run makes a handful of requests rather than hundreds, and short enough to finish well inside the idle timeout of any proxy in front of Pando — a held request cut off by one is a failure the caller did nothing to cause (issue #80).
const ReadTimeout = 10 * time.Minute
ReadTimeout is what one detection gets to read an app: a clone, an auction and a trial run that may pull a base image over a slow connection.
Generous: the cost of being too short is a detection that fails for a large repository on a home connection, which is exactly the user R-005 describes.
Variables ¶
var Timeout = ReadTimeout + time.Duration(spec.StandardDefaults().BuildTimeoutSeconds)*time.Second
Timeout bounds one detection run: reading the app, and building what it would deploy, which detection has done since R-311. The build gets what a deploy's build gets by default. Held to the reading's ten minutes, detection failed builds a deploy would have finished — on a six-CPU host with a dozen apps added at once, four in ten of them (issue #90).
Functions ¶
func Await ¶
func Await(ctx context.Context, r DetectionReader, c clock.Clock, appID string, wait time.Duration) (state.Detection, error)
Await returns an app's detection once it has moved on from what it was when Await was called — a new stage, or an outcome — or once wait has passed, whichever is first (issue #80).
A detection that is not running is returned at once: there is nothing to wait for, and holding a finished answer back would only look like a hang. So is one with no wait asked for, which is the plain GET.
A long poll rather than a stream, because it works through every proxy and fits MCP's one-request-one-answer shape, and every surface uses the same one (R-261). Reading the row on an interval rather than listening for a notification, because detection is recorded in exactly one place and this is the boring way to watch it.
func DefaultConcurrency ¶
func DefaultConcurrency() int
DefaultConcurrency is how many detections one replica runs at once when configuration does not say [P]: one per CPU, at least two. A detection is a clone and, sometimes, a trial run — lighter than a deploy, and just as unbounded when every app added at once starts one.
Types ¶
type AuditEvent ¶
AuditEvent is what a screening records.
type Auditor ¶
type Auditor interface {
Write(ctx context.Context, e AuditEvent) error
}
Auditor records that a repository's contents left the host (R-337).
type DetectionReader ¶
DetectionReader reads an app's current detection.
type ImageBuilder ¶ added in v0.5.0
type ImageBuilder interface {
BuildImages(ctx context.Context, s *spec.AppSpec, checkout *source.Checkout, sink io.Writer, tag string) (string, map[string]string, error)
BuiltImageAuth(ctx context.Context, image string) *api.RegistryAuth
}
ImageBuilder builds an app's images the way a deploy does. deploy.Runner is one.
type Installation ¶
type Installation interface {
// Defaults returns the install's adapters, routing mode and limits. The
// routing mode comes from the routing adapter's own declared default
// (R-162): adding an app uses it without asking.
Defaults(ctx context.Context) spec.Defaults
}
Installation supplies the answers a repository cannot give about itself.
A repository says it builds from a Dockerfile and listens on 3000. It cannot say which runtime this install uses or how apps here are addressed, and R-104 is explicit that none of that is a question worth asking — it is configuration, and configuration gets a default.
type PortAllocator ¶
type PortAllocator interface {
Allocate(ctx context.Context, adapterRef, appID string, from, to int) (int, error)
}
PortAllocator hands out host ports for port-mode routing.
type Queue ¶
type Queue struct {
Detections *state.Detections
// Detect runs one claimed detection to completion. In production,
// Runner.RunQueued.
Detect func(ctx context.Context, appID string) (state.Detection, error)
// Limit is how many detections this replica runs at once.
// DefaultConcurrency when zero.
Limit int
// Poll is how often to look for detections another replica queued. Two
// seconds when zero.
Poll time.Duration
// Timeout overrides the package Timeout. Zero is the default.
Timeout time.Duration
Logger *zap.Logger
// contains filtered or unexported fields
}
Queue runs queued detections on this replica (issue #72, O-32).
Enqueue marks an app's detection running and unclaimed; every replica's queue claims what it has room for. Creating a hundred apps at once used to start a hundred clones in the process that took the requests.
func (*Queue) Enqueue ¶
Enqueue queues a detection for an app — marked running, so the first poll cannot read the previous outcome as this one's — and tells this replica's queue to look now. It returns the detection as queued, from the statement that queued it (state.Detections.Queue): any replica may claim and finish it the moment it is written, and a later read could return that outcome.
type RegistryInstallation ¶
type RegistryInstallation struct {
Registry *api.Registry
// BaseDomain is what a subdomain is carved out of, in subdomain mode.
BaseDomain string
// Fallback supplies the values that come from neither an adapter nor host
// policy — the [P] defaults in the requirements.
Fallback spec.Defaults
}
RegistryInstallation reads an install's defaults from its configured adapters.
func NewInstallation ¶
func NewInstallation(registry *api.Registry, baseDomain string) *RegistryInstallation
NewInstallation returns an installation reading from registry.
func (*RegistryInstallation) Defaults ¶
func (i *RegistryInstallation) Defaults(ctx context.Context) spec.Defaults
Defaults resolves the install's adapters and routing mode.
The routing mode is asked of the routing adapter rather than configured centrally. R-162: each adapter declares a default mode, and adding an app uses it without asking — which is what makes an install feel like proxy mode or per-hostname without either being a global setting (design 03 §4.1).
type RegistryScreeners ¶
RegistryScreeners reads assignments from the adapter registry.
func (RegistryScreeners) ScreenerFor ¶
func (r RegistryScreeners) ScreenerFor(fn api.AIFunction) (screening.Screener, string, string, bool)
type Runner ¶
type Runner struct {
Apps *state.Apps
Detections *state.Detections
Job *detect.Job
// Sources fetches the source detection reads, including an uploaded one
// (R-262).
Sources source.Sources
// Policy is checked again here, not only at app creation. The allowlist can
// change between the two, and a re-detection (R-022) of an app whose source
// is no longer allowed must not clone it.
Policy SourcePolicy
// Install fills in everything the repository cannot answer. Without it a
// detected spec describes the app and says nothing about where it runs,
// which is a spec that cannot be planned — the gap that made detection
// produce proposals nobody could deploy.
Install Installation
// Ports assigns a host port in port-mode routing. Only consulted when the
// routing adapter's default mode is `port`, which the loopback adapter that
// ships as the laptop default is.
Ports PortAllocator
// PortRange bounds that allocation.
PortRangeStart int
PortRangeEnd int
// Screeners says which AI adapter performs each function, and on which
// model (R-259). Nil is the ordinary case: an install with no AI adapter
// configured is not a degraded install, because everything the auction
// produced is there either way (R-106, R-335).
Screeners Screeners
// Screener, when Screeners is nil, performs every function under
// ScreenerRef. For an embedding with one adapter and nothing to assign.
Screener screening.Screener
ScreenerRef string
ScreenPolicy ScreenPolicy
// Auditor records that a repository's contents left the host (R-337).
Auditor Auditor
// Screening budget (R-339). Zero means the package default.
ScreenMaxFiles int
ScreenMaxBytes int64
ScreenTimeout time.Duration
// Clock stamps conversation turns. Nil is the system clock.
Clock clock.Clock
// Scanner scores the checkout while it is still on disk (R-312).
//
// Detection is the first and, for a while, the only moment Pando holds an
// app's source: an app that has been added and not yet deployed has no
// image to look at, and waiting for one means the first thing anybody sees
// about a new app is "not scanned yet". A committed key or a vulnerable
// lockfile is exactly what somebody wants to know *before* deciding to
// deploy it.
//
// Optional, and never fatal. An installation with no scanner detects
// exactly as before, and a scanner that fails does not fail a detection —
// the proposal is the thing being produced here.
Scanner SourceScanner
// Builder builds what the app would deploy, so the plan can show the
// score of the image that will run (R-311). Nil builds nothing: the plan
// shows the source's findings and waits for the deploy to score the
// image.
Builder ImageBuilder
// NewBuildTag names a detection's build. Nil is a time-based name.
NewBuildTag func() string
// Background runs a rebuild after an answer (Rebuild). Nil is a
// goroutine.
Background func(func())
}
Runner detects for an app and stores the proposal.
func (*Runner) Built ¶ added in v0.5.0
Built is what detection built for an app, nil when it built nothing: what a deploy of the same build inputs runs instead of building (deploy DetectionBuildFinder).
func (*Runner) Check ¶
Check reports whether detection may run for an app, without starting it: the app exists, has a source, and the source is allowed (R-092).
Separate so a caller that runs detection in the background can refuse at once, and write nothing, when it would be refused anyway.
func (*Runner) Detect ¶
Detect runs detection for an app and records the result.
The allowlist is checked before anything touches disk, and the assertion in Sequence A is exact: a blocked source produces zero disk writes, because git clone is never invoked. That is why this check is here and not inside source.Fetch — a check that runs after the call has already started is not the same promise.
func (*Runner) Rebuild ¶ added in v0.5.0
Rebuild builds a finished proposal again when what it would build has changed since it was built — an answer to a question the build waited on, a build argument, a revised plan — and scans the new image, in the background. True when it started; the detection reads as running, at the building stage, until it has finished. Accepting waits for it, so the score a person accepts is the score of what the first deploy runs.
func (*Runner) Revise ¶
Revise asks the AI adapter to change an app's proposal as a person described, records the exchange on the proposal, and stores it.
type ScreenPolicy ¶
type ScreenPolicy interface {
// AllowsScreening returns a reason when host policy forbids it, and an
// empty string when it does not. A reason, not an error: this is not a
// failure, it is a posture, and it is shown in the review as one.
AllowsScreening(ctx context.Context) string
}
ScreenPolicy is host policy's veto over screening (R-336).
Its own interface rather than a method on SourcePolicy, because a nil one means "allowed" and every existing implementation of SourcePolicy would otherwise have had to grow a method saying so.
type Screeners ¶
type Screeners interface {
ScreenerFor(fn api.AIFunction) (s screening.Screener, ref, model string, ok bool)
}
Screeners resolves the adapter assigned an AI function (R-259).
type SourcePolicy ¶
type SourcePolicy interface {
AllowsSource(ctx context.Context, src spec.Source) error
IsolationFloors(ctx context.Context) (build, runtime spec.IsolationClass, err error)
}
SourcePolicy is the source allowlist check (R-092), plus the isolation floors a detected spec inherits (R-024, R-114).
type SourceScanner ¶
type SourceScanner interface {
// Scans reports whether there is a scanner to scan with.
Scans() bool
// ScanSource scans and records in one call.
ScanSource(ctx context.Context, req api.ScanRequest)
// ScanPart scans one part, the source or the image, recording nothing;
// RecordParts records the parts as one scan of both (R-311).
ScanPart(ctx context.Context, req api.ScanRequest) (api.ScanResult, error)
RecordParts(ctx context.Context, req api.ScanRequest, parts []api.ScanResult, failed error)
}
SourceScanner scans what detection read: the checkout, and the image the proposal runs — a published one, or the one detection built. Narrow, so detection cannot reach into the rest of the security service.