ai-sessions#13: capture's `summary` field wrote a frontmatter shape (title/harness/fidelity/captured_at/repos_touched) that ai-sessions' own extract/audit pipeline can't parse -- silently invisible to that repo's own audit tooling, and bypassing its redaction + Stage2 completeness gates by construction. Only 5 files ever landed this way over 3 weeks; the summary capability's whole value (speed) fights ai-sessions' whole value (redacted, audited, complete), so kill it rather than build a second parse branch. capture now persists insights -> brain and action items -> Gitea tickets only. Removes Summary/SummaryResult/SummaryWriter and all wiring (service, REST body, MCP tool schema); close-session updated to stop assembling a summary payload. specs/capture-*.md kept as historical record with a superseded note -- the feature shipped and is documented, just no longer current behaviour. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WPdSHbp9Utb2hPFm9wDG59
102 lines
4.1 KiB
Go
102 lines
4.1 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/capturehttp"
|
|
)
|
|
|
|
// principalKey is the context key under which the authenticated principal
|
|
// (re-derived in ServeHTTP) is stashed for the capture tool.
|
|
type principalKeyT struct{}
|
|
|
|
var principalKey principalKeyT
|
|
|
|
type principalInfo struct {
|
|
principal string
|
|
viaStatic bool
|
|
}
|
|
|
|
func withPrincipal(ctx context.Context, principal string, viaStatic bool) context.Context {
|
|
return context.WithValue(ctx, principalKey, principalInfo{principal: principal, viaStatic: viaStatic})
|
|
}
|
|
|
|
// captureToolDescriptor is the tools/list entry for the capture relay.
|
|
// Appended only when WithCapture has wired the tool.
|
|
func captureToolDescriptor() map[string]any {
|
|
str := func(d string) map[string]any { return map[string]any{"type": "string", "description": d} }
|
|
insightItem := map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"text": str("the insight body"), "wing": str("brain wing"),
|
|
"hall": str("brain hall (facts/decisions/failures/hypotheses/sources)"),
|
|
"supersede_slug": str("optional: slug of a prior note to revise in place instead of creating"),
|
|
},
|
|
"required": []string{"text", "wing", "hall"},
|
|
}
|
|
ticketItem := map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"repo": str("gitea repo (owner is always mathias)"), "action": str("create|close|comment"),
|
|
"number": map[string]any{"type": "integer", "description": "issue number (close/comment)"},
|
|
"title": str("issue title (create)"), "body": str("issue/comment body"),
|
|
},
|
|
"required": []string{"repo", "action"},
|
|
}
|
|
schema := map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"context": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"harness": str("descriptive harness label (telemetry only, never a gate input)"),
|
|
"session_ref": str("optional session reference"), "fidelity": str("live-capture|transcript-parse|agent-runlog"),
|
|
"actor": str("acting user/agent"), "classification": str("caller-declared sensitivity: public|internal|confidential"),
|
|
},
|
|
},
|
|
"insights": map[string]any{"type": "array", "items": insightItem},
|
|
"tickets": map[string]any{"type": "array", "items": ticketItem},
|
|
"dry_run": map[string]any{"type": "boolean", "description": "validate + return the would-be receipt, write nothing"},
|
|
},
|
|
}
|
|
b, _ := json.Marshal(schema)
|
|
return map[string]any{
|
|
"name": "capture",
|
|
"description": "Persist a session's value uniformly: insights → brain (write or supersede), action items → Gitea tickets. The relay door for MCP-native harnesses. Origin is server-derived from your authenticated identity; confidential captures through a us-nexus surface are refused (I1). Returns a partial-aware receipt.",
|
|
"inputSchema": json.RawMessage(b),
|
|
}
|
|
}
|
|
|
|
// brainCapture is the MCP capture tool: the #55 relay for MCP-native
|
|
// harnesses. It re-uses the same CaptureService, principal-derivation, and
|
|
// origin resolver as POST /capture — only the transport differs. It holds
|
|
// no state and retains nothing beyond the I5 audit record.
|
|
func (s *Server) brainCapture(ctx context.Context, args json.RawMessage) (json.RawMessage, error) {
|
|
if s.capture == nil {
|
|
return nil, fmt.Errorf("capture tool not configured")
|
|
}
|
|
info, ok := ctx.Value(principalKey).(principalInfo)
|
|
if !ok || info.principal == "" {
|
|
// No authenticated principal ⇒ cannot derive origin ⇒ cannot gate.
|
|
return nil, fmt.Errorf("capture requires an authenticated principal")
|
|
}
|
|
|
|
in, err := capturehttp.DecodeRequest(args)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid capture request: %w", err)
|
|
}
|
|
// Principal and origin are server-derived — never taken from the body.
|
|
in.Context.Principal = info.principal
|
|
in.Context.Origin = s.capture.resolver.Resolve(info.principal, info.viaStatic)
|
|
|
|
rec, err := s.capture.svc.Capture(ctx, in)
|
|
if err != nil {
|
|
// Surface I1/I5 refusals and validation failures verbatim; errors.Is
|
|
// markers (ErrSovereigntyRefused / ErrAuditUnavailable) ride in the message.
|
|
return nil, err
|
|
}
|
|
return json.Marshal(rec)
|
|
}
|