workspacescan

package
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 14, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package workspacescan deterministically detects a workspace's dev conventions (languages, package managers, egress registries, devcontainer/Dockerfile presence, tools, git remotes) from what's actually in the tree, not an LLM guess.

It follows internal/gitremote's conventions: read-only, bounded filepath.WalkDir (depth<=6), a manifest-count cap, a 1 MiB per-file read cap, no symlink following, control-char scrubbing, sorted+deduped output, no subprocess/exec, and fail-safe-to-empty — Scan and DeriveProfile never return an error; hitting a bound or an unrecognized build system just yields a lower-confidence profile, never a crash or a grant on uncertainty.

Two data shapes, isolation-critical:

  • ScanFacts is raw bounded evidence a scan emits; once it crosses a sandbox boundary it MUST be treated as untrusted.
  • WorkspaceProfile is the validated authority the control plane derives (DeriveProfile) and persists. SECURITY: its egress hosts come ONLY from the fixed markers.go table keyed on filenames — NEVER from file contents — so a hostile manifest body can't inject a host.

Index

Constants

View Source
const (
	ConfidenceHigh   = "high"
	ConfidenceMedium = "medium"
	ConfidenceLow    = "low"
)

Confidence buckets how much a WorkspaceProfile can be trusted without a human/AI review pass.

View Source
const (
	SourceDeterministic = "deterministic"
	SourceAIAssisted    = "ai_assisted"
)

Source records how a WorkspaceProfile was derived. Only SourceDeterministic ships today; SourceAIAssisted is reserved for a later AI fallback pass.

View Source
const EnvAsCodeDockerfilePath = genDockerfilePath

EnvAsCodeDockerfilePath exports genDockerfilePath so writeEnvAsCode (internal/api/workspace_envcode.go) can refuse to clobber a PRE-EXISTING hand-authored Dockerfile at this path — the one emitted file that must not be silently overwritten.

Variables

This section is empty.

Functions

func EmitEnvAsCode

func EmitEnvAsCode(p WorkspaceProfile, artifactBases map[string]string, baseRef string) (map[string]string, error)

EmitEnvAsCode produces committable environment-as-code from a scanned profile: a devcontainer.json (base + language features + standard agent-tool install + artifact-registry redirects) and an AGENTS.md documenting the detected toolchain and setup commands as prose, for a human/agent to run deliberately. Returned as path -> content.

artifactBases maps an artifact ecosystem to the operator's corporate registry base URL (URL-ONLY, never a token), already skipping NETWORK-ONLY rows; pass nil when no redirect is configured.

baseRef is the workspace's own resolved base-image ref, or "" — see baseOrBuild.

func GenerateDevcontainer

func GenerateDevcontainer(p WorkspaceProfile, pins Pins) (files map[string]string, err error)

GenerateDevcontainer produces a minimal, deterministic .devcontainer/devcontainer.json for the profile: universal base image plus one feature per detected language plus the standard agent-tool install, baked unconditionally via an always-emitted .devcontainer/Dockerfile. Returned map is path -> content, safe to feed straight to the envbuilder local-context build.

pins fixes the base, the features and the claude-code installer to what a registry or a record said; the zero Pins reproduces the unpinned files. The claude-code RUN also writes the pin file the builder reads back.

Pure: no I/O, no clock, no randomness. p.Languages is already sorted+deduped by DeriveProfile, so output bytes are identical for identical profiles.

func GeneratedInputs added in v0.9.0

func GeneratedInputs(p WorkspaceProfile, pins Pins) (types.ImageBuildInputs, error)

GeneratedInputs describes the inputs of the generated build for p under pins: the base, the features, the baked installers, and the hash of every file GenerateDevcontainer(p, pins) returns. It calls GenerateDevcontainer, so the record and the files cannot drift. A digest, version or checksum is filled only from a valid pin; an unpinned input has it empty.

func ShouldAdvise

func ShouldAdvise(base WorkspaceProfile, facts ScanFacts) bool

ShouldAdvise reports whether the AI fallback is worth invoking: only when the deterministic pass is LOW confidence or left UnrecognizedSamples. A high-confidence profile with nothing unresolved needs no AI and is returned unchanged by the caller (it never even calls AdviseProfile).

func ToolchainNeeds added in v0.5.0

func ToolchainNeeds(profiles ...WorkspaceProfile) (goNeeded, jvmNeeded bool)

ToolchainNeeds reports which toolchain-fidelity accommodations a run over these profiles needs. Requirements-driven, not platform-wide: Go's tempdir/cache redirect applies only when Go was detected; the Maven/Gradle JVM proxy sysprops only when that package manager was detected.

Types

type AIOptions

type AIOptions struct {
	Bin     string        // CLI binary path; empty → "claude" via PATH
	Timeout time.Duration // per-invocation bound; <=0 → aiDefaultTimeout
}

AIOptions configures the advisory CLI invocation. Zero values are safe: Bin defaults to "claude" (resolved via PATH), Timeout defaults to aiDefaultTimeout.

type GitRemotes

type GitRemotes struct {
	GitHub     []string `json:"github,omitempty"`
	OtherHosts []string `json:"other_hosts,omitempty"`
}

GitRemotes mirrors gitremote.DetectGitHubRepos's return shape: sorted "owner/repo" GitHub remotes and sorted non-GitHub hosts (for an operator warning / git_pat grant grounding).

type InstallerPin added in v0.9.0

type InstallerPin struct {
	Version, Platform, SHA256 string
}

InstallerPin is the version, platform and checksum an installer fetched.

func ParseInstallerPin added in v0.9.0

func ParseInstallerPin(b []byte) (InstallerPin, bool)

ParseInstallerPin parses the pin file a generated build writes, "<ver> <plat> <sum>\n", by the same rules as a pin. ok=false for anything else.

type LeakFinding

type LeakFinding struct {
	Path string `json:"path"`
	Kind string `json:"kind"`
	Line int    `json:"line,omitempty"`
	// Source names the attached source once multiple sources merge into one
	// profile; empty otherwise. A separate field rather than a Path prefix,
	// because Path is path-classified downstream (testdata|__tests__|fixtures)
	// and folding a locator in would misclassify leaks under a source whose
	// own path looks like a fixture.
	Source string `json:"source,omitempty"`
}

LeakFinding is a CONTENT-FREE report of a suspected committed secret VALUE. The leaked-value detector is the ONE lane that reads file values (to recognize a secret-shaped token), but it stores only WHERE and WHAT KIND — Kind is a detector id, never the matched bytes.

type ManifestHit

type ManifestHit struct {
	Path   string `json:"path"`   // slash-separated, relative to the scan root
	Marker string `json:"marker"` // canonical marker id, e.g. "package-lock.json"
}

ManifestHit is one recognized marker file found during a scan.

type Pins added in v0.9.0

type Pins struct {
	BaseDigest string
	Features   map[string]string // feature ref -> digest
	Installers map[string]InstallerPin
}

Pins is what a generated build is pinned to: a digest for the base, a digest per feature ref, and a version and checksum per installer tool. The zero Pins pins nothing. Pins come from a registry answer or from a stored record, and reach a FROM line, a devcontainer feature key and a shell, so every use goes through the accessors below, which ignore (leave unpinned) any value of the wrong shape.

func PinsFromInputs added in v0.9.0

func PinsFromInputs(in types.ImageBuildInputs) Pins

PinsFromInputs reads the pins out of a stored record's inputs, keeping only the values that are digests, versions and checksums.

type ScanFacts

type ScanFacts struct {
	ManifestsFound      []ManifestHit        `json:"manifests_found,omitempty"`
	GitRemotes          GitRemotes           `json:"git_remotes,omitempty"`
	HasDevcontainer     bool                 `json:"has_devcontainer,omitempty"`
	HasDockerfile       bool                 `json:"has_dockerfile,omitempty"`
	UnrecognizedSamples []UnrecognizedSample `json:"unrecognized_samples,omitempty"`
	Truncated           bool                 `json:"truncated,omitempty"`

	// Content-lane evidence: names/keys/hosts only, via anchored capture
	// groups — no file value ever lands here. UNTRUSTED until DeriveProfile
	// re-validates and caps it.
	SecretRequirements []SecretNeed  `json:"secret_requirements,omitempty"`
	ServicesFound      []string      `json:"services_found,omitempty"`
	SuggestedEgress    []string      `json:"suggested_egress,omitempty"`
	SecretFilesPresent []string      `json:"secret_files_present,omitempty"`
	BuildMemoryMiB     int           `json:"build_memory_mib,omitempty"`
	LeakFindings       []LeakFinding `json:"leak_findings,omitempty"`
	// Raw setup-command SIGNALS (not commands): which conventional script/target
	// keys exist. DeriveProfile synthesizes fixed-template SetupCommands from
	// these plus detected package managers — file content never becomes a command.
	ScriptKeys  []string `json:"script_keys,omitempty"`  // package.json scripts: build|test|lint present
	MakeTargets []string `json:"make_targets,omitempty"` // Makefile targets: build|test|install|lint present
	// BuildInputHashes maps a build-input file's path to a hex sha256 of its
	// content (devcontainer.json / Dockerfile) — a digest, not content, so safe.
	BuildInputHashes map[string]string `json:"build_input_hashes,omitempty"`
}

ScanFacts is the raw bounded evidence a scan emits — untrusted input to DeriveProfile, which always re-derives a WorkspaceProfile from these facts rather than trusting the producer.

func CollectFacts

func CollectFacts(root string) ScanFacts

CollectFacts walks root and collects the raw, bounded ScanFacts. This is the entry point the in-sandbox wardyn-scan binary calls: the control plane re-derives the untrusted facts into a WorkspaceProfile, never trusting them on faith (see DeriveProfile).

type SecretNeed

type SecretNeed struct {
	Name     string `json:"name"`
	Kind     string `json:"kind,omitempty"`
	Optional bool   `json:"optional,omitempty"`
}

SecretNeed is one secret/config key a workspace's committed files REFERENCE BY NAME — never a value. Detectors capture only the identifier before '='/':' or inside a ${...} placeholder; the rest of the line is discarded before anything is stored. Optional marks a safe default, commented template line, or deploy-time key — informational, never a launch blocker. Kind is a coarse family classification; "generic" when unknown.

type SetupCommand

type SetupCommand struct {
	Stage   string `json:"stage"`   // install | build | test | lint
	Command string `json:"command"` // fixed-template command, never file content
	Source  string `json:"source"`  // what implied it, e.g. "convention:go", "package.json:build"
}

SetupCommand is one conventional environment-setup step a workspace implies (install/build/test/lint). SECURITY: Command is NEVER copied from file content (a hostile package.json `scripts.build` could be `rm -rf`) — it's synthesized from a FIXED template keyed on the detected package manager and which conventional script/target KEYS exist. Advisory only: surfaced for operator review, executed only inside a confinement sandbox after explicit approval.

type UnrecognizedSample

type UnrecognizedSample struct {
	Path    string `json:"path"`
	Content string `json:"content"`
}

UnrecognizedSample is a bounded, scrubbed snippet of a file that looked like a build/dependency descriptor but isn't in the fixed marker table — evidence for a later AI fallback. Content is truncated and control-char stripped, never large enough to leak a secret value.

type WorkspaceProfile

type WorkspaceProfile struct {
	Languages       []string `json:"languages,omitempty"`
	PackageManagers []string `json:"package_managers,omitempty"`
	// ToolchainNeeds below reads these two: dispatch env derives from what
	// the scan actually detected, never a platform-wide guess.
	EgressDomains   []string   `json:"egress_domains,omitempty"`
	Tools           []string   `json:"tools,omitempty"`
	GitRemotes      GitRemotes `json:"git_remotes,omitempty"`
	HasDevcontainer bool       `json:"has_devcontainer,omitempty"`
	HasDockerfile   bool       `json:"has_dockerfile,omitempty"`

	// Advisory "needs" fields (content lane, validated by DeriveProfile)
	// inform the operator and never gate a launch or create a grant.
	// SECURITY: SuggestedEgress is content-derived and is NEVER auto-unioned
	// into a run's allowlist — only EgressDomains (filename-keyed) grants
	// that; an operator must promote hosts into ApprovedEgress explicitly.
	// These fields ride in ProfileHash, so a rescan after this changes
	// forces one harmless no-op image rebuild; hash only the image-affecting
	// subset instead if that churn ever bites.
	RequiredSecrets    []SecretNeed `json:"required_secrets,omitempty"`
	ServicesNeeded     []string     `json:"services_needed,omitempty"`
	SuggestedEgress    []string     `json:"suggested_egress,omitempty"`
	SecretFilesPresent []string     `json:"secret_files_present,omitempty"`
	// BuildMemoryMiB is the largest build-heap ceiling detected (JVM -Xmx /
	// Node --max-old-space-size). Advisory sizing hint only, never
	// auto-applied to a run's ResourceLimits.
	BuildMemoryMiB int `json:"build_memory_mib,omitempty"`
	// LeakFindings are content-free reports of suspected committed secret
	// values (path + detector kind + line, never the value). Advisory warning.
	LeakFindings []LeakFinding `json:"leak_findings,omitempty"`
	// SetupCommands are the conventional install/build/test/lint steps
	// implied, synthesized from fixed templates only. Advisory:
	// operator-approved, sandbox-only execution.
	SetupCommands []SetupCommand `json:"setup_commands,omitempty"`
	// ContextHash digests the build-input files' content (devcontainer.json /
	// Dockerfile); it rides ProfileHash so the built-image cache busts when a
	// build input changes even if the detected profile is unchanged. Empty
	// when no build-input files are present.
	ContextHash string `json:"context_hash,omitempty"`
	// Confidence is one of ConfidenceHigh/Medium/Low.
	Confidence  string `json:"confidence"`
	NeedsReview bool   `json:"needs_review,omitempty"`
	// Source is one of SourceDeterministic/SourceAIAssisted.
	Source string `json:"source"`
}

WorkspaceProfile is the validated, control-plane-owned authority derived from a scan. Every slice field is sorted + deduped; safe to persist and hand to run-creation for egress/grant/image decisions.

func AdviseProfile

func AdviseProfile(ctx context.Context, facts ScanFacts, base WorkspaceProfile, opts AIOptions) WorkspaceProfile

AdviseProfile runs the advisory AI fallback and merges its (advisory-only) result into a COPY of base. It FAILS OPEN: on any error the base profile is returned unchanged. It never overrides or deletes a deterministic fact — it only gap-fills EMPTY fields and can only RAISE NeedsReview.

func DeriveProfile

func DeriveProfile(facts ScanFacts) WorkspaceProfile

DeriveProfile re-derives a WorkspaceProfile from previously-emitted ScanFacts using the same logic Scan uses internally, so a local Scan and a marshal/unmarshal round trip always agree. Untrusted-input safe: an unknown Marker id is silently ignored, never trusted.

func MergeProfiles added in v0.5.0

func MergeProfiles(profiles []WorkspaceProfile, identities []string, primaryIdentity string) WorkspaceProfile

MergeProfiles combines N sources' individually-scanned profiles into ONE profile for the workspace: unions the set-like fields, concatenates leak findings and setup commands (deduped and re-capped), takes the largest build-memory hint and the LOWEST confidence, and folds ContextHash into a digest-of-digests. HasDevcontainer/HasDockerfile come from the PRIMARY source ONLY, matched against identities (not profiles[0], since attachment order and scan order can diverge) — no match yields false/false, never another source's values. SecretFilesPresent entries are prefixed "identity/path"; a LeakFinding instead carries its attribution in LeakFinding.Source and keeps Path scan-root-relative, since Path is path-CLASSIFIED downstream and a locator prefix would corrupt that. Empty input returns the zero profile.

func Scan

func Scan(root string) WorkspaceProfile

Scan walks root (bounded, read-only, no symlink following) and derives a WorkspaceProfile. It never returns an error: a scan that hits a bound or an unrecognized build system yields a lower-confidence profile, never a crash.

func (WorkspaceProfile) CacheKey added in v0.5.0

func (p WorkspaceProfile) CacheKey() string

CacheKey returns the SHA-256 digest keying a workspace's built-image cache: a salted digest of ProfileHash, not ProfileHash itself, so bumping cacheKeySalt forces a rebuild without colliding with it.

func (WorkspaceProfile) ProfileHash

func (p WorkspaceProfile) ProfileHash() string

ProfileHash returns the SHA-256 hex digest of the profile's canonical (sorted-key) JSON, so it cache-keys built images (BuiltProfileHash) the same regardless of Go struct field order.

A marshal → unmarshal-into-map → marshal round trip gets canonical JSON via encoding/json's sorted map-key output, without hand-rolling a key sort.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL