mirror of
https://github.com/coder/coder.git
synced 2026-06-07 15:08:20 +00:00
9da0cfe34b
P2: - CRF-7: effectiveAllowedRoots now appends the current working directory lazily so the manager picks up the resolved path after the manifest loads. - CRF-8: walkDir error path computes Kind from the basename (kindFromFilename) so .mcp.json and SKILL.md keep a stable resource ID across permission flips. - CRF-9: ComputeAggregateHash / writeLengthPrefixed docs now describe the Netstring-style encoding accurately. - CRF-10: resolveAndBroadcast snapshots inputs under the lock, releases it for the filesystem walk, then re-acquires to swap. - CRF-11: api.go SnapshotResource comment no longer references a nonexistent PayloadBase64 field. P3: - CRF-12: WatcherOptions.MaxDepth; the watcher now mirrors the resolver's depth instead of hardcoding DefaultMaxScanDepth. - CRF-13: drop obsolete "follow-up" sentence in doc.go. - CRF-14: Pusher doc says Agent API v2.10, not proto v30. - CRF-15: Resolver.ResolveContext threads ctx through walk so resolveAndBroadcast can cancel between roots. - CRF-16: new tests cover StatusUnreadable for instruction and MCP-config files (skipped on Windows and root). - CRF-17: TestRunPush_ClosesOnManagerClose covers closedCh path. - CRF-18: drop dead !ok branch on the subscriber channel. - CRF-19: extract readFileResource; readInstructionFile and readMCPConfig share the plumbing. - CRF-24: DefaultPushInitialBackoff / DefaultPushMaxBackoff. - CRF-25: subsequent-push test asserts AggregateHash and Version advance, not just the count. P4: - CRF-26: TestRunPush_RejectedResponseProceeds covers the Accepted=false fast-path. Nit: - CRF-27: slices.SortFunc/SortStableFunc replace sort.Slice. - CRF-28: firstLine uses strings.SplitSeq. - CRF-29: sync.OnceFunc replaces the sync.Once+closure wrapper. - CRF-30: atomic.Int32 replaces bare int32 in watch_test. - CRF-31: redundant sort in resolveLocked already gone with CRF-10. - CRF-32: drop redundant "agentcontext:" prefix from log messages. - CRF-33: resolveAndBroadcast doc no longer claims conditional. Note: - CRF-34: ResourceStatus doc explains the proto-enum offset. - CRF-35: readInstructionFile doc flags the sanitization gap delegated to the chatd follow-up.
228 lines
7.3 KiB
Go
228 lines
7.3 KiB
Go
package agentcontext
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// ResourceKind describes the category of a resolved context
|
|
// resource. The values mirror the proto ContextResource.Kind
|
|
// enum reserved in the RFC; future kinds (PLUGIN, HOOK,
|
|
// SUBAGENT, COMMAND) are defined here so callers can switch
|
|
// exhaustively, but no v1 resolver emits them.
|
|
type ResourceKind int
|
|
|
|
const (
|
|
KindUnspecified ResourceKind = iota
|
|
// KindInstructionFile covers AGENTS.md, CLAUDE.md,
|
|
// .cursorrules, and similar plain-text rule files that
|
|
// inject content into the model prompt.
|
|
KindInstructionFile
|
|
// KindSkill is a directory containing SKILL.md and any
|
|
// supporting files. Only the meta file is read at
|
|
// resolve time; bodies are fetched on demand.
|
|
KindSkill
|
|
// KindMCPConfig is a .mcp.json fragment declaring one or
|
|
// more MCP servers.
|
|
KindMCPConfig
|
|
// KindMCPServer is a live MCP server's resolved tool list,
|
|
// populated by an MCPProvider after the server has been
|
|
// connected.
|
|
KindMCPServer
|
|
// KindPlugin is reserved for Claude Code plugin manifests.
|
|
// Not emitted by v1.
|
|
KindPlugin
|
|
// KindHook is reserved for plugin hooks. Not emitted by v1.
|
|
KindHook
|
|
// KindSubagent is reserved for plugin-declared subagents.
|
|
// Not emitted by v1.
|
|
KindSubagent
|
|
// KindCommand is reserved for plugin slash commands.
|
|
// Not emitted by v1.
|
|
KindCommand
|
|
)
|
|
|
|
// String returns the lower-snake-case name used in IDs and
|
|
// metrics. Unknown values stringify to "unknown".
|
|
func (k ResourceKind) String() string {
|
|
switch k {
|
|
case KindInstructionFile:
|
|
return "instruction_file"
|
|
case KindSkill:
|
|
return "skill"
|
|
case KindMCPConfig:
|
|
return "mcp_config"
|
|
case KindMCPServer:
|
|
return "mcp_server"
|
|
case KindPlugin:
|
|
return "plugin"
|
|
case KindHook:
|
|
return "hook"
|
|
case KindSubagent:
|
|
return "subagent"
|
|
case KindCommand:
|
|
return "command"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// ResourceStatus describes whether a resource was successfully
|
|
// read and whether its payload survived the per-resource and
|
|
// aggregate caps.
|
|
//
|
|
// Note: these iota ordinals do NOT match the proto
|
|
// ContextResource.Status ordinals one-to-one. The proto enum
|
|
// reserves 0 for STATUS_UNSPECIFIED and shifts every value by
|
|
// one, so the conversion in resourceStatusToProto cannot be
|
|
// replaced with a direct int cast. ResourceKind, by contrast,
|
|
// does align with its proto counterpart.
|
|
type ResourceStatus int
|
|
|
|
const (
|
|
// StatusOK indicates the payload was populated.
|
|
StatusOK ResourceStatus = iota
|
|
// StatusOversize indicates the resource exceeded the
|
|
// per-resource size cap; payload is omitted.
|
|
StatusOversize
|
|
// StatusUnreadable indicates an IO error reading the
|
|
// resource (permission denied, broken symlink, etc.).
|
|
StatusUnreadable
|
|
// StatusInvalid indicates the resource was structurally
|
|
// malformed (bad JSON, missing front-matter, etc.).
|
|
StatusInvalid
|
|
// StatusExcluded indicates the resource was dropped to fit
|
|
// the aggregate snapshot or count cap.
|
|
StatusExcluded
|
|
)
|
|
|
|
// String returns the lower-snake-case name used in IDs and
|
|
// metrics. Unknown values stringify to "unknown".
|
|
func (s ResourceStatus) String() string {
|
|
switch s {
|
|
case StatusOK:
|
|
return "ok"
|
|
case StatusOversize:
|
|
return "oversize"
|
|
case StatusUnreadable:
|
|
return "unreadable"
|
|
case StatusInvalid:
|
|
return "invalid"
|
|
case StatusExcluded:
|
|
return "excluded"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// Source is a user-declared scan root added to the agent's
|
|
// in-memory list via the HTTP API or boot-time env seeding.
|
|
// Identity is the canonical absolute path.
|
|
type Source struct {
|
|
// Path is the canonical absolute path (symlinks resolved,
|
|
// ~ expanded). Empty means the zero value.
|
|
Path string
|
|
}
|
|
|
|
// Resource is what the resolver emits for each recognized file
|
|
// or live server it discovers under a scan root.
|
|
type Resource struct {
|
|
// ID is stable across pushes for the same logical
|
|
// resource. The current scheme is "<kind>:<source>".
|
|
ID string
|
|
// Kind classifies the resource for snapshot consumers.
|
|
Kind ResourceKind
|
|
// Source is the file path or MCP server name.
|
|
Source string
|
|
// ContentHash is sha256 over the resource's original
|
|
// bytes (or transport-encoded server tool list).
|
|
ContentHash [32]byte
|
|
// Payload is the full bytes when Status == StatusOK; the
|
|
// per-resource and aggregate caps may leave it empty.
|
|
Payload []byte
|
|
// SizeBytes is the original payload size, populated
|
|
// regardless of Status.
|
|
SizeBytes uint64
|
|
// Status records OK or a reason the payload is absent.
|
|
Status ResourceStatus
|
|
// Error is populated whenever Status != StatusOK; may
|
|
// also carry a non-fatal warning when Status == StatusOK.
|
|
Error string
|
|
// Description is a short human-readable summary (skill
|
|
// front-matter description, MCP server description, etc.).
|
|
Description string
|
|
// SourcePath is the user-declared source that contributed
|
|
// the resource; empty for built-in scan roots.
|
|
SourcePath string
|
|
}
|
|
|
|
// Snapshot is the immutable bundle of resources produced by a
|
|
// single resolver pass.
|
|
type Snapshot struct {
|
|
// Version is monotonically increasing per Manager
|
|
// instance; resets when the agent process restarts.
|
|
Version uint64
|
|
// SchemaVersion is bumped if the resource shape on the
|
|
// wire changes.
|
|
SchemaVersion uint64
|
|
// AggregateHash is sha256 over a canonical encoding of
|
|
// (ID, Kind, Source, ContentHash, Status) for every
|
|
// resource. Identical inputs always produce identical
|
|
// hashes; see ComputeAggregateHash.
|
|
AggregateHash [32]byte
|
|
// Resources is sorted by ID for deterministic encoding.
|
|
Resources []Resource
|
|
// PayloadBytes is the sum of len(Resource.Payload) across
|
|
// emitted resources after caps were applied.
|
|
PayloadBytes uint64
|
|
// SnapshotError carries a single snapshot-level error
|
|
// string when present (count cap exceeded, watcher
|
|
// degraded, ENOSPC, etc.). Empty when healthy.
|
|
SnapshotError string
|
|
}
|
|
|
|
// ComputeAggregateHash produces the deterministic snapshot
|
|
// aggregate hash for the supplied resources. The caller does
|
|
// not need to pre-sort; the function sorts a copy of the slice
|
|
// to keep its inputs side-effect free.
|
|
//
|
|
// The encoding is a Netstring-style stream. Each string field
|
|
// is written as the decimal-ASCII length, the literal ':', and
|
|
// the raw UTF-8 bytes. ContentHash is written as 32 raw bytes
|
|
// without a length prefix because it is a fixed-size SHA-256
|
|
// digest. Resources are separated by a single NUL byte. The
|
|
// scheme is internal to the agent and coderd, but it is stable
|
|
// across platforms because every field has an unambiguous
|
|
// length.
|
|
func ComputeAggregateHash(resources []Resource) [32]byte {
|
|
indexed := make([]Resource, len(resources))
|
|
copy(indexed, resources)
|
|
slices.SortFunc(indexed, func(a, b Resource) int {
|
|
return strings.Compare(a.ID, b.ID)
|
|
})
|
|
|
|
h := sha256.New()
|
|
for _, r := range indexed {
|
|
writeLengthPrefixed(h, r.ID)
|
|
writeLengthPrefixed(h, r.Kind.String())
|
|
writeLengthPrefixed(h, r.Source)
|
|
_, _ = h.Write(r.ContentHash[:])
|
|
writeLengthPrefixed(h, r.Status.String())
|
|
_, _ = h.Write([]byte{0})
|
|
}
|
|
var out [32]byte
|
|
copy(out[:], h.Sum(nil))
|
|
return out
|
|
}
|
|
|
|
// writeLengthPrefixed writes a decimal-ASCII length prefix, a
|
|
// literal ':' separator, and the raw bytes of s. This matches
|
|
// the Netstring framing used by ComputeAggregateHash.
|
|
func writeLengthPrefixed(h interface{ Write([]byte) (int, error) }, s string) {
|
|
_, _ = h.Write([]byte(strconv.Itoa(len(s))))
|
|
_, _ = h.Write([]byte{':'})
|
|
_, _ = h.Write([]byte(s))
|
|
}
|