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
- func EmitEnvAsCode(p WorkspaceProfile, artifactBases map[string]string, baseRef string) (map[string]string, error)
- func GenerateDevcontainer(p WorkspaceProfile, pins Pins) (files map[string]string, err error)
- func GeneratedInputs(p WorkspaceProfile, pins Pins) (types.ImageBuildInputs, error)
- func ShouldAdvise(base WorkspaceProfile, facts ScanFacts) bool
- func ToolchainNeeds(profiles ...WorkspaceProfile) (goNeeded, jvmNeeded bool)
- type AIOptions
- type GitRemotes
- type InstallerPin
- type LeakFinding
- type ManifestHit
- type Pins
- type ScanFacts
- type SecretNeed
- type SetupCommand
- type UnrecognizedSample
- type WorkspaceProfile
- func AdviseProfile(ctx context.Context, facts ScanFacts, base WorkspaceProfile, opts AIOptions) WorkspaceProfile
- func DeriveProfile(facts ScanFacts) WorkspaceProfile
- func MergeProfiles(profiles []WorkspaceProfile, identities []string, primaryIdentity string) WorkspaceProfile
- func Scan(root string) WorkspaceProfile
Constants ¶
const ( ConfidenceHigh = "high" ConfidenceMedium = "medium" ConfidenceLow = "low" )
Confidence buckets how much a WorkspaceProfile can be trusted without a human/AI review pass.
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.
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 ¶
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 ¶
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.