Build a module from a repository and a path within it

The builder cloned a repository and read the manifest at its root, which means one
repository per module. Nothing we have is shaped that way, so the builder could be
asked to build nothing that exists (novox/hq ADR 0069).

The path travels the whole way — named when asking, carried in the request, used
to read the manifest and as the context everything is produced from, echoed back
in the result, and recorded as part of where a module came from. Without that last
part the mesh could notice a module was behind its source and then be unable to
rebuild it, which is the worst of both.

A path climbing out of the clone is refused: a machine whose job is building other
people's repositories must not read whatever else is on its disk.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
2026-09-12 16:45:50 +02:00
parent c4030947b0
commit f151de103f
7 changed files with 160 additions and 33 deletions
+44 -5
View File
@@ -57,7 +57,7 @@ type Result struct {
// archive failed would otherwise leave half of itself in the store under a digest the mesh never
// records — reachable, unreferenced, and indistinguishable from something in use.
func Build(ctx context.Context, run Runner, publish Publisher,
repository, ref, workspace string) (Result, error) {
repository, path, ref, workspace string) (Result, error) {
// Made rather than required. A builder that fails because the directory it was told to work
// in does not exist is a builder that needs a setup step nobody documented.
@@ -85,11 +85,20 @@ func Build(ctx context.Context, run Runner, publish Publisher,
}
commit = strings.TrimSpace(commit)
raw, err := os.ReadFile(filepath.Join(tree, ManifestName))
// A module is a repository and a path within it (novox/hq ADR 0069). The ordinary case is an
// empty path, meaning the repository's root; a repository holding several modules names each
// by its own directory, which is what the catalogue is and what the system this replaces has
// always done.
within, err := inside(tree, path)
if err != nil {
return Result{}, err
}
raw, err := os.ReadFile(filepath.Join(within, ManifestName))
if err != nil {
return Result{}, fmt.Errorf(
"%s has no %s at its root, so there is nothing saying what it is: %w",
repository, ManifestName, err)
"%s has no %s at %s, so there is nothing saying what it is: %w",
repository, ManifestName, describe(path), err)
}
manifest, err := catalogue.ParseManifest(raw)
if err != nil {
@@ -103,7 +112,7 @@ func Build(ctx context.Context, run Runner, publish Publisher,
// logs can be compared.
sort.Slice(artifacts, func(i, j int) bool { return artifacts[i].Name < artifacts[j].Name })
for _, a := range artifacts {
made, err := one(ctx, run, publish, manifest.Module, tree, commit, a)
made, err := one(ctx, run, publish, manifest.Module, within, commit, a)
if err != nil {
return Result{}, err
}
@@ -118,6 +127,36 @@ func Build(ctx context.Context, run Runner, publish Publisher,
return Result{Manifest: resolved, Commit: commit, Built: built}, nil
}
// inside resolves a module's path within a clone, and refuses one that leaves it.
//
// **A build reads only its own tree.** A path of `../../etc` would otherwise make a build read —
// and an archive artifact publish — whatever the build machine happens to hold, which is the one
// thing a machine that builds other people's repositories must not do.
func inside(tree, path string) (string, error) {
if path == "" {
return tree, nil
}
if filepath.IsAbs(path) {
return "", fmt.Errorf(
"a module's path is inside its repository, and %q is an absolute path", path)
}
within := filepath.Join(tree, path)
rel, err := filepath.Rel(tree, within)
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "", fmt.Errorf(
"%q leaves the repository, and a build reads only its own tree", path)
}
return within, nil
}
// describe says where a manifest was looked for, in words a person can act on.
func describe(path string) string {
if path == "" {
return "its root"
}
return path
}
// ManifestName is the one file a module repository must have.
//
// At the root, and named the same in every repository. A convention somebody can look for beats a