Publish immutable remote commits

This commit is contained in:
2026-08-10 20:05:24 +00:00
parent d6deccf3e8
commit 361dbb4ca8
15 changed files with 725 additions and 251 deletions

View File

@@ -19,6 +19,7 @@ type RemoteArtifactType string
const (
RemoteArtifactTypeSessionManifest RemoteArtifactType = "session_manifest"
RemoteArtifactTypeRunManifest RemoteArtifactType = "run_manifest"
RemoteArtifactTypeRunFile RemoteArtifactType = "run_file"
RemoteArtifactTypePublishedOutput RemoteArtifactType = "published_output"
RemoteArtifactTypePreviousArtifact RemoteArtifactType = "previous_artifact"
)
@@ -68,15 +69,38 @@ func (m RemoteCommitManifest) Validate() error {
return fmt.Errorf("remote commit artifacts are required")
}
destinations := make(map[string]struct{}, len(m.Artifacts))
if err := ValidateRemoteArtifactMapping(m.Artifacts); err != nil {
return err
}
for index, artifact := range m.Artifacts {
if err := artifact.Validate(); err != nil {
return fmt.Errorf("remote commit artifact %d: %w", index, err)
}
}
return nil
}
// ValidateRemoteArtifactMapping verifies that source and destination identities
// form one unambiguous immutable-object mapping.
func ValidateRemoteArtifactMapping(artifacts []RemoteArtifact) error {
if len(artifacts) == 0 {
return fmt.Errorf("remote commit artifacts are required")
}
destinations := make(map[string]struct{}, len(artifacts))
sources := make(map[string]struct{}, len(artifacts))
for index, artifact := range artifacts {
if err := validateRemoteArtifactIdentity(artifact); err != nil {
return fmt.Errorf("remote commit artifact %d: %w", index, err)
}
if _, exists := destinations[artifact.DestinationKey]; exists {
return fmt.Errorf("remote commit declares duplicate destination %q", artifact.DestinationKey)
}
destinations[artifact.DestinationKey] = struct{}{}
sourceKey := string(artifact.Type) + "\x00" + artifact.Source
if _, exists := sources[sourceKey]; exists {
return fmt.Errorf("remote commit declares ambiguous %s source %q", artifact.Type, artifact.Source)
}
sources[sourceKey] = struct{}{}
}
return nil
}
@@ -98,16 +122,8 @@ func (m RemoteCommitManifest) ValidateForSessionPrefix(sessionPrefix string) err
}
func (a RemoteArtifact) Validate() error {
switch a.Type {
case RemoteArtifactTypeSessionManifest, RemoteArtifactTypeRunManifest, RemoteArtifactTypePublishedOutput, RemoteArtifactTypePreviousArtifact:
default:
return fmt.Errorf("unsupported artifact type %q", a.Type)
}
if strings.TrimSpace(a.Source) == "" {
return fmt.Errorf("source is required")
}
if err := validateRemoteObjectKey(a.DestinationKey); err != nil {
return fmt.Errorf("destination key: %w", err)
if err := validateRemoteArtifactIdentity(a); err != nil {
return err
}
if err := validateSHA256(a.SHA256); err != nil {
return fmt.Errorf("sha256: %w", err)
@@ -121,6 +137,21 @@ func (a RemoteArtifact) Validate() error {
return nil
}
func validateRemoteArtifactIdentity(a RemoteArtifact) error {
switch a.Type {
case RemoteArtifactTypeSessionManifest, RemoteArtifactTypeRunManifest, RemoteArtifactTypeRunFile, RemoteArtifactTypePublishedOutput, RemoteArtifactTypePreviousArtifact:
default:
return fmt.Errorf("unsupported artifact type %q", a.Type)
}
if strings.TrimSpace(a.Source) == "" {
return fmt.Errorf("source is required")
}
if err := validateRemoteObjectKey(a.DestinationKey); err != nil {
return fmt.Errorf("destination key: %w", err)
}
return nil
}
func (p CurrentCommitPointer) Validate() error {
if p.FormatVersion != RemoteCommitFormatVersion {
return fmt.Errorf("unsupported current commit pointer format version %d", p.FormatVersion)

View File

@@ -64,6 +64,12 @@ func S3RunCommitKey(sessionPrefix, runID string) string {
return path.Join(strings.TrimSuffix(S3RunPrefix(sessionPrefix, runID), "/"), config.S3CommitFile)
}
// S3RunSessionManifestKey returns the immutable session-manifest snapshot key for one run.
// Format: {session_prefix}/runs/{run_id}/session-manifest.json
func S3RunSessionManifestKey(sessionPrefix, runID string) string {
return path.Join(strings.TrimSuffix(S3RunPrefix(sessionPrefix, runID), "/"), config.S3SessionSnapshotFile)
}
// S3CurrentCommitPointerKey returns the sole mutable selector for a committed run.
// Format: {session_prefix}/current/commit-pointer.json
func S3CurrentCommitPointerKey(sessionPrefix string) string {

View File

@@ -35,6 +35,9 @@ func TestS3KeyConstruction(t *testing.T) {
if got, want := S3RunCommitKey(sessionPrefix, runID), wantRunPrefix+"commit.json"; got != want {
t.Fatalf("commit key = %q, want %q", got, want)
}
if got, want := S3RunSessionManifestKey(sessionPrefix, runID), wantRunPrefix+"session-manifest.json"; got != want {
t.Fatalf("session manifest key = %q, want %q", got, want)
}
runPointer := S3CurrentRunPointerKey(sessionPrefix)
if runPointer != "dnd/campaigns/forsaken/sessions/2026-04-19/current/run_id.txt" {