Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

131 lines
4.4 KiB
Go

package agenthost
import (
"context"
"fmt"
"strings"
storesqlite "github.com/tutti-os/tutti/packages/agent/store-sqlite"
)
// RailPlacementVersion is the current Host rail-placement contract version.
const RailPlacementVersion = 1
func normalizeRailPlacement(placement *RailPlacement) (*RailPlacement, error) {
if placement == nil {
return nil, nil
}
normalized := &RailPlacement{
Version: placement.Version,
Kind: RailPlacementKind(strings.TrimSpace(string(placement.Kind))),
ProjectPath: strings.TrimSpace(placement.ProjectPath),
SectionKey: strings.TrimSpace(placement.SectionKey),
}
if normalized.Version != RailPlacementVersion {
return nil, fmt.Errorf("%w: unsupported rail placement version", ErrInvalidArgument)
}
switch normalized.Kind {
case RailPlacementKindConversations:
normalized.ProjectPath = ""
normalized.SectionKey = storesqlite.RailSectionKeyConversations
case RailPlacementKindProject:
normalized.ProjectPath = storesqlite.NormalizeProjectPath(normalized.ProjectPath)
if normalized.ProjectPath == "" {
key := storesqlite.NormalizeRailSectionKey(normalized.SectionKey)
if strings.HasPrefix(key, "project:") {
normalized.ProjectPath = storesqlite.NormalizeProjectPath(
strings.TrimPrefix(key, "project:"),
)
}
}
if normalized.ProjectPath == "" {
return nil, fmt.Errorf("%w: invalid project rail placement", ErrInvalidArgument)
}
normalized.SectionKey = storesqlite.RailSectionKeyForProject(normalized.ProjectPath)
default:
return nil, fmt.Errorf("%w: invalid rail placement kind", ErrInvalidArgument)
}
return normalized, nil
}
func railPlacementMatchesSession(placement *RailPlacement, session storesqlite.Session) bool {
if placement == nil {
return true
}
return strings.TrimSpace(session.RailSectionKind) == string(placement.Kind) &&
storesqlite.NormalizeProjectPath(session.RailProjectPath) ==
storesqlite.NormalizeProjectPath(placement.ProjectPath) &&
storesqlite.NormalizeRailSectionKey(session.RailSectionKey) ==
storesqlite.NormalizeRailSectionKey(placement.SectionKey)
}
func railPlacementFromSession(session storesqlite.Session) (*RailPlacement, error) {
kind := RailPlacementKind(strings.TrimSpace(session.RailSectionKind))
projectPath := strings.TrimSpace(session.RailProjectPath)
sectionKey := storesqlite.NormalizeRailSectionKey(session.RailSectionKey)
if kind == "" {
switch {
case sectionKey == "", sectionKey == storesqlite.RailSectionKeyConversations:
kind = RailPlacementKindConversations
case strings.HasPrefix(sectionKey, "project:"):
kind = RailPlacementKindProject
}
}
if kind == RailPlacementKindConversations && sectionKey == "" {
sectionKey = storesqlite.RailSectionKeyConversations
}
if kind == RailPlacementKindProject && projectPath == "" && strings.HasPrefix(sectionKey, "project:") {
projectPath = strings.TrimPrefix(sectionKey, "project:")
}
if kind == RailPlacementKindConversations && projectPath == "" {
return normalizeRailPlacement(&RailPlacement{
Version: RailPlacementVersion, Kind: RailPlacementKindConversations,
SectionKey: storesqlite.RailSectionKeyConversations,
})
}
return normalizeRailPlacement(&RailPlacement{
Version: RailPlacementVersion,
Kind: kind,
ProjectPath: projectPath,
SectionKey: sectionKey,
})
}
func runtimeEnvironmentForCanonicalSession(
env []string,
cwd string,
session storesqlite.Session,
) ([]string, error) {
placement, err := railPlacementFromSession(session)
if err != nil {
return nil, err
}
return withAgentRailPlacementEnvironment(env, cwd, placement)
}
// GetSessionWithRailPlacement reads one canonical Session only when its
// immutable rail identity matches the caller's Host-normalized placement.
// Recovery consumers use this boundary instead of reproducing rail
// normalization or comparing canonical storage fields outside Agent Host.
func (h *Host) GetSessionWithRailPlacement(
ctx context.Context,
ref SessionRef,
placement *RailPlacement,
) (GetSessionResult, error) {
normalized, err := normalizeRailPlacement(placement)
if err != nil {
return GetSessionResult{}, err
}
if normalized == nil {
return GetSessionResult{}, ErrInvalidArgument
}
result, err := h.GetSession(ctx, ref)
if err != nil {
return GetSessionResult{}, err
}
if !railPlacementMatchesSession(normalized, result.Canonical) {
return GetSessionResult{}, ErrRailPlacementConflict
}
return result, nil
}