package app import ( "context" "crypto/sha256" "errors" "fmt" "strings" "sync" "time" "agent-system-backend/internal/projectcontext" "agent-system-backend/internal/runtime" "agent-system-backend/pkg/extension" core "agent-system-core" ) type projectContextSlot struct { mu sync.RWMutex holder string provider extension.ProjectContextProvider } func (s *projectContextSlot) active() bool { if s == nil { return false } s.mu.RLock() defer s.mu.RUnlock() return s.provider != nil } func requireProjectContextProvider(required bool, slot *projectContextSlot) error { if required && !slot.active() { return errors.New("extension: required project context provider not assembled") } return nil } func (r compiledRegistry) RegisterProjectContextProvider(extension.ProjectContextProvider) error { return errors.New("extension: declarative plugins cannot register project context providers") } func (r pluginRegistry) RegisterProjectContextProvider(p extension.ProjectContextProvider) error { if r.caps.contexts == nil || r.pluginID == "" || p == nil { return errors.New("extension: project context provider unavailable") } slot := r.caps.contexts slot.mu.Lock() defer slot.mu.Unlock() if slot.provider != nil { return errors.New("extension: project context provider already registered") } slot.holder, slot.provider = r.pluginID, p return nil } func (r recordRegistry) RegisterProjectContextProvider(p extension.ProjectContextProvider) error { reg, ok := r.Registry.(extension.ProjectContextRegistry) if !ok { return errors.New("extension: project context registration unavailable") } return reg.RegisterProjectContextProvider(p) } func (s *projectContextSlot) resolve(ctx context.Context, req extension.ProjectContextRequest) (extension.ProjectContextBundle, error) { if s == nil { return extension.ProjectContextBundle{}, errors.New("extension: project context provider missing") } s.mu.RLock() p, holder := s.provider, s.holder s.mu.RUnlock() if p == nil { return extension.ProjectContextBundle{}, errors.New("extension: project context provider missing") } if req.Actor == "" || req.ProjectID == "" { return extension.ProjectContextBundle{}, errors.New("extension: trusted project scope missing") } req.PluginID = holder bounded, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() b, err := p.Resolve(bounded, req) if err != nil { return extension.ProjectContextBundle{}, err } if err := bounded.Err(); err != nil { return extension.ProjectContextBundle{}, err } if b.ProjectID != req.ProjectID || b.Status != extension.ProjectContextReady { return extension.ProjectContextBundle{}, fmt.Errorf("extension: project context unavailable: %s", b.Status) } return b, nil } func (s *projectContextSlot) readEvidence(ctx context.Context, req extension.ProjectEvidenceRequest) (extension.ProjectEvidence, error) { if s == nil || req.Scope.Actor == "" || req.Scope.ProjectID == "" || req.Reference.ID == "" { return extension.ProjectEvidence{}, errors.New("extension: trusted evidence scope missing") } s.mu.RLock() p, holder := s.provider, s.holder s.mu.RUnlock() if p == nil { return extension.ProjectEvidence{}, errors.New("extension: project context provider missing") } req.Scope.PluginID = holder bounded, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() evidence, err := p.ReadEvidence(bounded, req) if err != nil { return extension.ProjectEvidence{}, err } if err := bounded.Err(); err != nil { return extension.ProjectEvidence{}, err } if evidence.Reference != req.Reference || len([]rune(evidence.Text)) > req.Scope.BudgetChars { return extension.ProjectEvidence{}, errors.New("extension: project evidence response mismatches reference or budget") } return evidence, nil } func (s *projectContextSlot) recheck(ctx context.Context, req extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error) { // Each unusable scope is reported on its own so host-side logs name the // actual defect; every branch still rejects (fail-closed). if s == nil { return extension.ProjectContextSourceRecheckResult{}, errors.New("extension: project context source recheck slot missing") } if req.Scope.Actor == "" { return extension.ProjectContextSourceRecheckResult{}, errors.New("extension: trusted source recheck scope has no actor") } if req.Scope.ProjectID == "" { return extension.ProjectContextSourceRecheckResult{}, errors.New("extension: trusted source recheck scope has no project") } if req.Scope.Goal != "" { return extension.ProjectContextSourceRecheckResult{}, errors.New("extension: source recheck scope carries a generation goal") } s.mu.RLock() p, holder := s.provider, s.holder s.mu.RUnlock() rechecker, ok := p.(extension.ProjectContextSourceRechecker) if !ok { return extension.ProjectContextSourceRecheckResult{}, errors.New("extension: project source rechecker unavailable") } req.Scope.PluginID = holder bounded, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() result, err := rechecker.Recheck(bounded, req) if err != nil { return extension.ProjectContextSourceRecheckResult{}, err } if err := bounded.Err(); err != nil { return extension.ProjectContextSourceRecheckResult{}, err } if result.Bundle.ProjectID != req.Scope.ProjectID || result.Bundle.Status != extension.ProjectContextReady { return extension.ProjectContextSourceRecheckResult{}, errors.New("extension: project sources no longer authorized") } return result, nil } const workContextBudgetChars = 12000 // TaskCreateForWork fixes the project binding while the authenticated command // dispatch still owns its conversation. Legacy TaskCreate has no such binding. func (h inprocHost) TaskCreateForWork(kind, title string) (string, error) { if h.actor == "" || h.actor != h.owner || h.pluginID == "" || h.conversationID == "" || h.caps.st == nil || !h.caps.contexts.active() { return "", errors.New("extension: trusted project work dispatch required") } conv, err := h.caps.st.GetConversation(h.owner, string(h.conversationID)) if err != nil { return "", err } if conv.ProjectID == "" || (h.hostProjectID != "" && h.hostProjectID != conv.ProjectID) || h.caps.projects == nil || h.caps.projects.work() == nil { return "", errors.New("extension: trusted project work scope missing") } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() allowed, err := h.caps.projects.work()(ctx, h.actor, conv.ProjectID) if err != nil || !allowed { return "", errors.New("extension: project work denied") } task, err := h.caps.st.CreateWorkTask(h.owner, h.pluginID, string(conv.ProjectID), string(conv.ID), kind, title) return task.ID, err } // completeForWork is the dispatch-scoped completion path. The conversation is // reloaded so a plugin's goal/target and a stale dispatch project cannot select // another project's context. Work authorization is checked on every call. func (h inprocHost) CompleteForWork(ctx context.Context, req extension.WorkCompletionRequest) (extension.WorkCompletionResult, error) { if strings.TrimSpace(req.Goal) == "" { return extension.WorkCompletionResult{}, errors.New("extension: work goal required") } if h.caps.st == nil { return extension.WorkCompletionResult{}, errors.New("extension: trusted work store missing") } actor, convID, targetID := h.actor, h.conversationID, string(h.conversationID) var boundProjectID core.ID if actor == "" { if req.TaskID == "" || convID != "" { return extension.WorkCompletionResult{}, errors.New("extension: persisted work task required for scoped host") } binding, err := h.caps.st.WorkTaskScope(h.owner, h.pluginID, req.TaskID) if err != nil { return extension.WorkCompletionResult{}, err } if binding.Status == "done" || binding.Status == "completed" || binding.Status == "failed" || binding.Status == "cancelled" { return extension.WorkCompletionResult{}, errors.New("extension: work task already terminal") } actor, convID, boundProjectID, targetID = binding.Owner, core.ID(binding.ConversationID), core.ID(binding.ProjectID), binding.TaskID } else if actor != h.owner || convID == "" || req.TaskID != "" { return extension.WorkCompletionResult{}, errors.New("extension: trusted work dispatch required") } conv, err := h.caps.st.GetConversation(h.owner, string(convID)) if err != nil { return extension.WorkCompletionResult{}, err } if conv.ProjectID == "" || (h.hostProjectID != "" && h.hostProjectID != conv.ProjectID) || (boundProjectID != "" && boundProjectID != conv.ProjectID) { return extension.WorkCompletionResult{}, errors.New("extension: trusted project scope mismatch") } if h.caps.projects == nil || h.caps.projects.work() == nil { return extension.WorkCompletionResult{}, errors.New("extension: project work authorization missing") } allowed, err := h.caps.projects.work()(ctx, actor, conv.ProjectID) if err != nil { return extension.WorkCompletionResult{}, err } if !allowed { return extension.WorkCompletionResult{}, errors.New("extension: project work denied") } requested := extension.ProjectContextRequest{ Actor: actor, ProjectID: conv.ProjectID, ConversationID: conv.ID, TaskID: core.ID(req.TaskID), Goal: req.Goal, TargetID: req.TargetID, BudgetChars: workContextBudgetChars, } bundle, err := h.caps.contexts.resolve(ctx, requested) if err != nil { return extension.WorkCompletionResult{}, err } evidenceText, usedEvidence, err := h.readWorkEvidence(ctx, requested, bundle, req.EvidenceRefs) if err != nil { return extension.WorkCompletionResult{}, err } prompt, manifest, err := assembleWorkCompletionWithEvidence(req.Goal, bundle, workContextBudgetChars, evidenceText, usedEvidence) if err != nil { return extension.WorkCompletionResult{}, err } manifest.ProjectID = conv.ProjectID manifest.TaskID = req.TaskID route := runtime.ResolveChatModel(core.AgentProfile{}, h.caps.set.Effective(h.owner, "")) manifest.Model = route.Model if route.Model == "" || route.Base == "" { return extension.WorkCompletionResult{}, errors.New("llm route not configured") } // Resolve can be slow. A member may be revoked while the provider is // assembling summaries, so check the live work policy again at send time. allowed, err = h.caps.projects.work()(ctx, actor, conv.ProjectID) if err != nil { return extension.WorkCompletionResult{}, err } if !allowed { return extension.WorkCompletionResult{}, errors.New("extension: project work denied") } current, err := h.caps.contexts.resolve(ctx, requested) if err != nil { return extension.WorkCompletionResult{}, err } if err := projectcontext.ValidateCurrent(manifest, current); err != nil { return extension.WorkCompletionResult{}, err } for _, used := range manifest.ReadEvidence { found := false for _, ref := range current.Evidence { if ref.ID != used.ReferenceID || ref.SourceID != used.SourceID || ref.Revision != used.Revision || ref.Hash != used.SourceHash { continue } scope := requested scope.BudgetChars = 8000 now, err := h.caps.contexts.readEvidence(ctx, extension.ProjectEvidenceRequest{Scope: scope, Reference: ref}) if err != nil { return extension.WorkCompletionResult{}, err } if fmt.Sprintf("%x", sha256.Sum256([]byte(now.Text))) == used.InputSHA256 { found = true } break } if !found { return extension.WorkCompletionResult{}, errors.New("extension: requested work evidence changed before model send") } } attemptID, err := projectcontext.NewAttemptID() if err != nil { return extension.WorkCompletionResult{}, err } manifest.AttemptID, manifest.Delivery = attemptID, "attempted" if err := h.caps.st.AppendContextManifest(h.owner, targetID, manifest); err != nil { return extension.WorkCompletionResult{}, err } text, err := completeChat(ctx, route.Base, route.Key, route.Model, prompt) if err != nil { manifest.Delivery = "unknown" _ = h.caps.st.AppendContextManifest(h.owner, targetID, manifest) return extension.WorkCompletionResult{}, err } manifest.Delivery = "responded" if err := h.caps.st.AppendContextManifest(h.owner, targetID, manifest); err != nil { return extension.WorkCompletionResult{}, err } return extension.WorkCompletionResult{Text: text, Manifest: manifest}, nil } func assembleWorkCompletion(goal string, bundle extension.ProjectContextBundle, budget int) (string, extension.ProjectContextManifest, error) { return assembleWorkCompletionWithEvidence(goal, bundle, budget, "", nil) } func assembleWorkCompletionWithEvidence(goal string, bundle extension.ProjectContextBundle, budget int, evidenceText string, used []extension.ProjectContextManifestEntry) (string, extension.ProjectContextManifest, error) { prefix := "Work goal:\n" + goal + "\n\n" ref, m, err := projectcontext.Assemble(bundle, budget-len([]rune(prefix))-len([]rune(evidenceText))) if err != nil { return "", m, err } prompt := prefix + ref + evidenceText m.BudgetChars = budget m.ReadEvidence = used m.InputSHA256 = fmt.Sprintf("%x", sha256.Sum256([]byte(prompt))) return prompt, m, nil } func (h inprocHost) readWorkEvidence(ctx context.Context, scope extension.ProjectContextRequest, bundle extension.ProjectContextBundle, ids []string) (string, []extension.ProjectContextManifestEntry, error) { if len(ids) > 4 { return "", nil, errors.New("extension: too many work evidence references") } var b strings.Builder var used []extension.ProjectContextManifestEntry seen := make(map[string]bool, len(ids)) for _, id := range ids { if id == "" || seen[id] { return "", nil, errors.New("extension: invalid or duplicate work evidence reference") } seen[id] = true var selected extension.ProjectEvidenceRef for _, ref := range bundle.Evidence { if ref.ID == id { selected = ref break } } if selected.ID == "" { return "", nil, errors.New("extension: work evidence reference not in current bundle") } readScope := scope readScope.BudgetChars = 8000 evidence, err := h.caps.contexts.readEvidence(ctx, extension.ProjectEvidenceRequest{Scope: readScope, Reference: selected}) if err != nil { return "", nil, err } b.WriteString("\n[Evidence ") b.WriteString(selected.ID) b.WriteString(" source ") b.WriteString(selected.SourceID) b.WriteString(" revision ") b.WriteString(selected.Revision) b.WriteString("]\n") b.WriteString(evidence.Text) used = append(used, extension.ProjectContextManifestEntry{ReferenceID: selected.ID, SourceID: selected.SourceID, Revision: selected.Revision, SourceHash: selected.Hash, InputSHA256: fmt.Sprintf("%x", sha256.Sum256([]byte(evidence.Text)))}) } return b.String(), used, nil }