feat: collect workspace logs in support bundles (#26694)

Add workspace-side file collection to `coder support bundle` via
repeatable --workspace-file flags. The agent resolves the requested
paths or globs inside the remote workspace and streams back a tar with
a manifest and the collected files; nothing is read from the machine
running the command.

- Add POST /api/v0/bundle-files to the agent's agentfiles package.
- Expand env vars in the agent's environment; paths must then be
  absolute or start with ~/ (the agent user's home directory).
- Support ** globs and tail oversized files.
- Record requested patterns, per-path errors, truncation, and the
  applied limits in a manifest.
- Unpack the archive into the bundle under agent/workspace_files/,
  recording dropped entries in collection_errors.txt.
- Write a manifest-only archive marking collection as unsupported for
  agents that predate the endpoint.
- Bound collection: 64 KB request body, 10000 files, 10 MiB per file,
  100 MiB total including archive overhead, 110 MiB client-side read
  cap, 5 minute timeout.

Closes #26020
This commit is contained in:
Ehab Younes
2026-07-16 13:00:32 +03:00
committed by GitHub
parent f997afa220
commit 35ade9e3d2
19 changed files with 1390 additions and 71 deletions
+25 -5
View File
@@ -9,8 +9,16 @@ import (
)
// lexicalPath returns raw as a cleaned, absolute path with ~
// expanded and symlinks left unresolved.
// expanded against the current user's home and symlinks left
// unresolved.
func lexicalPath(raw string) (string, error) {
return lexicalPathIn(os.UserHomeDir, raw)
}
// lexicalPathIn is lexicalPath with home injected. home is called
// only when a ~ prefix needs expanding, so an absolute path resolves
// even when home is unavailable.
func lexicalPathIn(home func() (string, error), raw string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", xerrors.New("path is empty")
@@ -18,14 +26,14 @@ func lexicalPath(raw string) (string, error) {
// ~user forms are intentionally unsupported.
if raw == "~" || strings.HasPrefix(raw, "~/") {
home, err := os.UserHomeDir()
h, err := home()
if err != nil {
return "", xerrors.Errorf("expand home dir: %w", err)
}
if raw == "~" {
raw = home
raw = h
} else {
raw = filepath.Join(home, raw[2:])
raw = filepath.Join(h, raw[2:])
}
}
@@ -40,7 +48,19 @@ func lexicalPath(raw string) (string, error) {
// CanonicalizePath returns lexicalPath with symlinks resolved
// when the target exists.
func CanonicalizePath(raw string) (string, error) {
cleaned, err := lexicalPath(raw)
return resolveCanonicalPath(os.UserHomeDir, raw)
}
// CanonicalizePathIn is CanonicalizePath with ~ expanded against
// the given home directory instead of the current user's.
func CanonicalizePathIn(home string, raw string) (string, error) {
return resolveCanonicalPath(func() (string, error) { return home, nil }, raw)
}
// resolveCanonicalPath implements both CanonicalizePath and
// CanonicalizePathIn.
func resolveCanonicalPath(home func() (string, error), raw string) (string, error) {
cleaned, err := lexicalPathIn(home, raw)
if err != nil {
return "", err
}
+11
View File
@@ -66,6 +66,17 @@ func TestCanonicalizePath_BareTildeExpandsToHome(t *testing.T) {
require.Equal(t, want, got)
}
func TestCanonicalizePathIn_ExpandsAgainstGivenHome(t *testing.T) {
t.Parallel()
home := testutil.TempDirResolved(t)
got, err := agentcontext.CanonicalizePathIn(home, "~/.coder")
require.NoError(t, err)
require.Equal(t, filepath.Join(home, ".coder"), got)
_, err = agentcontext.CanonicalizePathIn(home, "relative/path")
require.Error(t, err)
}
func TestCanonicalizePath_FollowsSymlinks(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {