Documentation
¶
Overview ¶
Package workspacescan deterministically detects a local directory's (or a cloned repo's) development conventions — languages, package managers, implied egress registries, dev-container/Dockerfile presence, tools, and git remotes — so Wardyn can onboard a workspace with a profile grounded in what's ACTUALLY in the tree, not an LLM guess.
It clones internal/gitremote's conventions: read-only, bounded filepath.WalkDir (depth<=4), a manifest-count cap and a 1 MiB per-file read cap, NO symlink following, a control-char scrub on any string that crosses out of the scan, sorted+deduped output, NO subprocess/exec, and fail-safe-to-empty — Scan and DeriveProfile never return an error; a scan that hits a bound or an unrecognized build system just yields a lower-confidence profile, never a crash or a grant on uncertainty.
Two data shapes (A2, isolation-critical split):
- ScanFacts is raw bounded evidence a scan emits. When it comes from a sandboxed repo scan (governed run, a later wave) it crosses the sandbox boundary and MUST be treated as untrusted.
- WorkspaceProfile is the validated authority the control plane derives (DeriveProfile) and persists. Egress hosts in a WorkspaceProfile ONLY ever come 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 EmitArtifactConfig(bases map[string]string) (files map[string]string, env map[string]string)
- func EmitEnvAsCode(p WorkspaceProfile, approved []SetupCommand, artifactBases map[string]string) (map[string]string, error)
- func GenerateDevcontainer(p WorkspaceProfile) (files map[string]string, err error)
- func HostOf(rawURL string) string
- func MaskSecretShaped(s string) string
- func PublicRegistryHosts(ecosystem string) []string
- func ShouldAdvise(base WorkspaceProfile, facts ScanFacts) bool
- func ValidApprovedHost(h string) bool
- func ValidSetupCommand(c SetupCommand) bool
- type AIOptions
- type GitRemotes
- type LeakFinding
- type ManifestHit
- type ScanFacts
- type SecretNeed
- type SetupCommand
- type UnrecognizedSample
- type VerifyResult
- type VerifyStepResult
- type 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. Wave 1 only ever produces SourceDeterministic; SourceAIAssisted is reserved for the (later, not-this-wave) AI fallback pass.
Variables ¶
This section is empty.
Functions ¶
func EmitArtifactConfig ¶
EmitArtifactConfig turns operator-configured artifact-registry redirects (ecosystem -> corporate base URL) into the per-tool config each toolchain reads to pull from the corporate mirror instead of the public registry. Returns (files, env):
- files: path -> content, keyed by each tool's real config location relative to HOME (npm .npmrc, pip .config/pip/pip.conf, cargo .cargo/config.toml, maven .m2/settings.xml, nuget .nuget/NuGet/NuGet.Config). A dispatch-time writer drops them under $HOME; a committable export drops the repo-cascading ones (.npmrc/.cargo) usefully at the repo root and the rest as documentation.
- env: the go-toolchain variables (go redirects via GOPROXY/GOSUMDB env, not a file).
The output is URL-ONLY and carries NO secret — an injected registry token is applied proxy-side, never written into a committable/readable config file. Maven's settings.xml is intentionally MIRRORS-ONLY: the sandbox reaches the mirror THROUGH wardyn-proxy via MAVEN_OPTS (set platform-wide at dispatch), so no <proxies> block is emitted here — which also keeps a committed settings.xml free of the sandbox-only wardyn-proxy hostname (mirrors=which-URL is additive to proxies=how-to-reach, which lives in MAVEN_OPTS). GOPRIVATE is deliberately NOT set: GOPRIVATE="*" would route modules to direct VCS and defeat the corp GOPROXY, and GOSUMDB=off already disables the checksum DB the corp proxy may not serve. Pure + deterministic; unknown/empty ecosystems are skipped.
Injection safety: base URLs come from site-config, which validateSiteConfig already rejects if they contain control chars or shell/XML metacharacters (`$;&|<>"'\), so embedding base verbatim into TOML/XML/ini here is safe.
func EmitEnvAsCode ¶
func EmitEnvAsCode(p WorkspaceProfile, approved []SetupCommand, artifactBases map[string]string) (map[string]string, error)
EmitEnvAsCode produces committable environment-as-code from a VERIFIED profile + its operator-approved setup commands: a devcontainer.json (base + language features + the install/build steps as postCreateCommand) and an AGENTS.md documenting the detected toolchain and setup commands. Returned as path -> content. The install/build stages become postCreateCommand (env setup); test/lint are documented in AGENTS.md but not auto-run on create.
artifactBases maps an artifact ecosystem (npm|pip|cargo|maven|go|nuget) to the operator's corporate registry base URL (from the persisted site-config, URL-ONLY — never a token). When non-empty, the matching per-tool config files (and go's containerEnv) are merged in so a committed workspace pulls from the corporate mirror; pass nil when no redirect is configured.
func GenerateDevcontainer ¶
func GenerateDevcontainer(p WorkspaceProfile) (files map[string]string, err error)
GenerateDevcontainer produces a minimal, deterministic .devcontainer/devcontainer.json for the profile: the universal base image plus one official devcontainer feature per detected, feature-supported language. The returned map is path -> file content (a single entry); it is safe to feed straight to the envbuilder local-context build (BuildFromDevcontainerFiles).
Pure: no I/O, no clock, no randomness. p.Languages is already sorted+deduped by DeriveProfile, so iterating it and letting encoding/json sort the features map yields identical bytes for identical profiles.
func MaskSecretShaped ¶
MaskSecretShaped replaces any high-precision leaked-value match (AWS/GitHub/ Stripe/JWT/…) with a fixed placeholder, so a build log that happened to print a secret-shaped token never persists it. Reuses the leaked-value catalog.
func PublicRegistryHosts ¶
PublicRegistryHosts returns the public-registry hosts a corporate redirect replaces for an artifact ecosystem (npm|pip|go|cargo|maven|nuget), or nil for an unknown key. The egress-substitution layer drops these and adds the corp host when the operator configures a redirect for that ecosystem.
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 ValidApprovedHost ¶
ValidApprovedHost reports whether h is a plain lowercase dotted host of the exact shape the content lane emits into SuggestedEgress — the only shape the approved-egress API accepts for operator promotion (no scheme, port, path, or wildcard; wildcards remain a policy-allowlist privilege).
func ValidSetupCommand ¶
func ValidSetupCommand(c SetupCommand) bool
ValidSetupCommand reports whether c is an acceptable operator-approved setup command: a known stage and a single-line, bounded, control-char-free command (the operator vouches for what it DOES — it runs confined — but the string must be safe to store, audit, and stream). Source is advisory metadata and is not validated for content beyond length.
Types ¶
type AIOptions ¶
type AIOptions struct {
Bin string // CLI binary path; empty → Tool name via PATH
Tool string // "claude" (default) or "codex"
Model string // model id; empty → the CLI's own default
Timeout time.Duration // per-invocation bound; <=0 → aiDefaultTimeout
}
AIOptions configures the advisory CLI invocation. Zero values are safe: Tool defaults to claude, Bin defaults to the tool name (resolved via PATH), Timeout defaults to aiDefaultTimeout, Model lets the CLI pick its default.
type GitRemotes ¶
type GitRemotes struct {
GitHub []string `json:"github,omitempty"`
OtherHosts []string `json:"other_hosts,omitempty"`
}
GitRemotes mirrors internal/gitremote.DetectGitHubRepos's (github, otherHosts) return shape as a named, JSON-friendly struct: the sorted "owner/repo" GitHub remotes found, and the sorted set of non-GitHub remote HOSTS (for an operator warning / git_pat grant grounding).
type LeakFinding ¶
type LeakFinding struct {
Path string `json:"path"`
Kind string `json:"kind"`
Line int `json:"line,omitempty"`
}
LeakFinding is a CONTENT-FREE report of a suspected committed secret VALUE. The leaked-value detector (detect.go) is the ONE lane that reads file values — to recognize a secret-shaped token — but it stores only WHERE and WHAT KIND, never the matched bytes: Kind is a detector id ("aws-access-key", "github-pat", ...), never the value. This mirrors internal/contentscan's content-free Finding discipline.
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 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 (detect.go): names/keys/hosts only, extracted via
// anchored capture groups — no file value ever lands here. Like every other
// fact these are UNTRUSTED until DeriveProfile re-validates and caps them.
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 + the 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 rel path to a hex sha256 of its
// CONTENT (devcontainer.json / Dockerfile). A digest, not content — safe.
BuildInputHashes map[string]string `json:"build_input_hashes,omitempty"`
}
ScanFacts is the raw bounded evidence a scan emits. It is untrusted input to DeriveProfile: a WorkspaceProfile is always re-derived from these facts control-plane-side, never taken on faith from whatever produced them.
func CollectFacts ¶
CollectFacts walks root and collects the raw, bounded ScanFacts. This is the entry point the in-sandbox wardyn-scan binary calls: it emits the untrusted facts a governed repo scan ships back over the brokered scan-result route, which the control plane re-derives into a WorkspaceProfile (never trusting the facts 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 (detect.go) capture only the identifier left of the '='/':' delimiter or inside a ${...} placeholder; the rest of the line is discarded before anything is stored. Optional means the file declared a safe default (Spring `${VAR:default}`), a commented template line, or a deploy-time key (SealedSecret) — surfaced for information, never a launch blocker. Kind is a coarse env-name-family classification ("postgres", "oidc", "deploy", ... — see classifySecretKind); "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 dependencies, build, test, or lint. SECURITY: the Command string is NEVER copied from a file's content (a hostile package.json `scripts.build` could be `rm -rf`); it is synthesized from a FIXED template keyed on the detected package manager + which conventional script/target KEYS exist — exactly the filename-keyed discipline egress hosts use. Advisory only: a command is surfaced for operator review and only ever executed inside a confinement sandbox after explicit approval (mirrors SuggestedEgress).
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 the (later) AI fallback. Content is truncated and has control characters stripped; it is never large enough, nor selected in a way, to leak a secret value.
type VerifyResult ¶
type VerifyResult struct {
Steps []VerifyStepResult `json:"steps,omitempty"`
OK bool `json:"ok"`
// Ran is false when there were no approved commands to run.
Ran bool `json:"ran"`
// Done distinguishes a final upload (finalize the workspace status) from an
// intermediate PROGRESS upload (keep `verifying`, just show the steps so far).
Done bool `json:"done,omitempty"`
// Total is how many commands will run (so the UI can show "step N of Total").
Total int `json:"total,omitempty"`
// FailureHint explains the FIRST failing/timed-out step in operator terms,
// when its exit code + log tail match a known environmental signature
// (missing toolchain, Maven proxy, noexec /tmp) — see classifyFailureHint.
// Empty when there is no failure or none of the signatures matched (the
// existing "Suggest a fix from denied egress" flow stays the fallback).
FailureHint string `json:"failure_hint,omitempty"`
}
VerifyResult is a verify run's outcome, uploaded by wardyn-verify (PROGRESS uploads with Done=false as each step starts/finishes, then a final Done=true upload) and re-validated by DeriveVerifyResult control-plane-side.
func DeriveVerifyResult ¶
func DeriveVerifyResult(raw VerifyResult) VerifyResult
DeriveVerifyResult re-validates an untrusted VerifyResult upload: caps step count, coerces stage/command/exit to bounded/known values, masks secret-shaped tokens out of logs, bounds log length, and recomputes OK from the step exit codes (never trusting the uploader's OK flag). Mirrors DeriveProfile's facts-out-not-authority-out discipline.
type VerifyStepResult ¶
type VerifyStepResult struct {
Stage string `json:"stage"`
Command string `json:"command"`
// Running marks the step currently executing in a PROGRESS upload (no exit
// code yet). A final upload never carries Running steps.
Running bool `json:"running,omitempty"`
ExitCode int `json:"exit_code"`
DurationMs int64 `json:"duration_ms,omitempty"`
TimedOut bool `json:"timed_out,omitempty"`
LogHead string `json:"log_head,omitempty"`
LogTail string `json:"log_tail,omitempty"`
}
VerifyStepResult is one setup command's outcome. LogHead/LogTail are a bounded rolling head+tail of combined stdout/stderr (so both the setup context and the failure survive truncation).
type WorkspaceProfile ¶
type WorkspaceProfile struct {
Languages []string `json:"languages,omitempty"`
PackageManagers []string `json:"package_managers,omitempty"`
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).
// RequiredSecrets/ServicesNeeded/SecretFilesPresent inform the operator
// (needs panel, setup checklist) and never gate a launch or create a
// grant. SuggestedEgress is content-derived and is NEVER auto-unioned
// into a run's allowlist (that privilege is EgressDomains-only, which
// stays filename-keyed) — an operator promotes hosts into the
// workspace's operator-owned ApprovedEgress list instead.
// ponytail: these advisory fields ride in ProfileHash, so a workspace's
// first rescan after this change forces one no-op image rebuild. Ceiling:
// harmless one-time churn; upgrade path — hash only the image-affecting
// subset (Languages/HasDevcontainer/HasDockerfile) if it 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: surfaced so an operator can size
// the sandbox; 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 this
// workspace implies, synthesized from fixed templates (never file content).
// Advisory: operator-approved before they ever run, and only in a sandbox.
SetupCommands []SetupCommand `json:"setup_commands,omitempty"`
// ContextHash is a digest of the BUILD-INPUT files' CONTENT (a repo's own
// devcontainer.json / Dockerfile). It rides ProfileHash so the built-image
// cache (BuiltProfileHash) busts when a build input changes even if the
// detected profile is otherwise identical — the gap a profile-only hash has
// for the repo-owns-its-devcontainer build path. Empty when no build-input
// files are present (generated-devcontainer path is already profile-derived).
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. It is safe to persist and to hand to run-creation for egress/grant/image decisions (A6, a later wave).
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 is the control-plane re-derivation of a WorkspaceProfile from previously-emitted ScanFacts — the SAME logic Scan uses internally, so a local Scan(root) and a facts round trip (Scan → marshal → unmarshal → DeriveProfile) always agree. Untrusted-input safe: an unknown Marker id (e.g. from a future scanner version) is silently ignored, never trusted.
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) ProfileHash ¶
func (p WorkspaceProfile) ProfileHash() string
ProfileHash returns the SHA-256 hex digest of the profile's canonical (sorted-object-keys) JSON form. It's used to cache-key generated/built images (Workspace.BuiltProfileHash, a later wave): the same detected profile always hashes the same, regardless of Go struct field order.
encoding/json marshals a map[string]any with its keys sorted, so a marshal → unmarshal-into-map → marshal round trip is a standard-library-only way to get canonical JSON without hand-rolling a key sort.