hyperguild#79's caller audit (done in a prior session) found the removal was safe: - The promote/pending flow (#38/#81, ingestion/internal/api/promote.go) operates exclusively on brainDir/raw/ -> brainDir/wiki/<wing>/<hall>/. It never touches brainDir/knowledge/, so removing the knowledge/ write path does not break it. - No live caller uses domain/type or the knowledge/ path: checked hyperguild itself, mathias/skills, mathias/agentsquad, mathias/dispatch, mathias/cad, mathias/cad-atlas. - internal/skills/brain (a skill.go + handlers.go pair) defined a third, even-more-legacy brain_write shape (type/domain/filename, no wing/hall) for an old "supervisor" MCP server whose cmd/supervisor/main.go no longer exists. Nothing outside its own test imported it; deleted entirely as it encodes the exact legacy shape this issue removes. Changes: - ingestion/internal/mcp/handlers.go: brain_write's tool schema and brainWriteArgs drop type/domain; wing+hall are now marked required in the schema. - ingestion/internal/api/handler.go: WriteNote has one behaviour — wing+ hall required, routing to wiki/<wing>/<hall>/. writeLegacyNote and the knowledge/-path branch are deleted. Omitting wing or hall now returns "wing and hall are required" (400 over HTTP, a tool error over MCP) — the only sane failure mode left once there is no other valid destination. - ingestion/internal/capture/ports.go, internal/brainstore/store.go: drop Type/Domain from capture.Note and its WriteNoteOptions construction. pipeline/*.go's unrelated "domain" field (a different, legitimate concept for brain_ingest_raw) is untouched. - internal/skills/brain/: deleted (orphaned legacy tool schema, dead since cmd/supervisor was removed). Tests: added TestWriteNote_RequiresWingAndHall (api), TestBrainWriteRequiresWingAndHall + TestBrainWriteIgnoresLegacyTypeAndDomainParams (mcp) for the new single behaviour; removed tests that only exercised the deleted legacy path (TestWrite_IncludesFrontmatterWhenTypeProvided, TestWriteNote_HallRouteExistingTypeWinsOverOptsType, TestBrainWriteRejectsTraversal, TestBrainWriteAcceptsDoubleDotInName — the latter two's filename-sanitisation behaviour is covered independently by internal/brain/path_test.go's TestNotePath_SanitisesWingAndSlug / TestSanitise, which is unaffected by this change). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
123 lines
4.4 KiB
Go
123 lines
4.4 KiB
Go
package capture
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/classification"
|
|
)
|
|
|
|
// Ref is the read-after-write handle returned by a brain write/update —
|
|
// the #45 contract. ContentHash lets the caller confirm what landed
|
|
// without a re-query; for an Update, Superseded is true.
|
|
type Ref struct {
|
|
ID string
|
|
Path string
|
|
ContentHash string
|
|
Superseded bool
|
|
// Unlinked names the [[wikilink]] targets the store unwrapped to plain
|
|
// text because they resolve to no entry. Reported rather than rejected:
|
|
// see brain.StripUnresolvedLinks for why the write still lands.
|
|
Unlinked []string
|
|
}
|
|
|
|
// StoredNote is a brain note fetched by Get: the read-after-write
|
|
// confirmation primitive (a direct fetch, never a semantic query).
|
|
type StoredNote struct {
|
|
ID string
|
|
Path string
|
|
ContentHash string
|
|
Frontmatter map[string]string
|
|
Body string
|
|
}
|
|
|
|
// Note is the brain-write payload: wing+hall are the only routing fields
|
|
// (the legacy type/domain params were removed — ADR-0012 / #79). Reason is
|
|
// the supersede rationale, used only by Update.
|
|
type Note struct {
|
|
Content string
|
|
Filename string
|
|
Wing string
|
|
Hall string
|
|
Reason string
|
|
}
|
|
|
|
// BrainStore is the brain persistence port — the shared implementation of
|
|
// the #45 write/update/get verbs that both the MCP handlers and capture
|
|
// call, so there is one implementation, not two. The read-after-write +
|
|
// staleness discipline lives behind this interface so no caller carries
|
|
// the rule.
|
|
type BrainStore interface {
|
|
Write(ctx context.Context, n Note) (Ref, error)
|
|
Update(ctx context.Context, slug string, n Note) (Ref, error)
|
|
Get(ctx context.Context, id string) (StoredNote, error)
|
|
}
|
|
|
|
// IssueRef identifies a ticket touched by the tracker.
|
|
type IssueRef struct {
|
|
Repo string
|
|
Number int
|
|
URL string
|
|
}
|
|
|
|
// IssueTracker is the Gitea ticket port. The implementation (#52) always
|
|
// scopes to owner "mathias"; the port deliberately omits owner.
|
|
type IssueTracker interface {
|
|
CreateIssue(ctx context.Context, repo, title, body string) (IssueRef, error)
|
|
// CloseIssue closes an issue, optionally posting a closing comment
|
|
// first (empty comment ⇒ close only).
|
|
CloseIssue(ctx context.Context, repo string, number int, comment string) (IssueRef, error)
|
|
CommentIssue(ctx context.Context, repo string, number int, body string) (IssueRef, error)
|
|
}
|
|
|
|
// ClassificationPolicy derives a target's sensitivity (model C). The
|
|
// "stricter wins" combination of declared vs derived is use-case policy
|
|
// and lives in the service, so the port stays minimal. Satisfied by
|
|
// classification.Config (#50).
|
|
type ClassificationPolicy interface {
|
|
Derive(target classification.Target) classification.Level
|
|
}
|
|
|
|
// AuditEntry is the request-level audit record (I5): who/what captured
|
|
// what, when, via which principal. SecurityEvents carries anomalies such
|
|
// as a caller under-declaring sensitivity relative to the target floor.
|
|
type AuditEntry struct {
|
|
Timestamp time.Time
|
|
Principal string
|
|
Actor string
|
|
Harness string
|
|
SessionRef string
|
|
EffectiveClassification string
|
|
Items []string
|
|
SecurityEvents []string
|
|
}
|
|
|
|
// AuditOutcome is how a capture's audit record was (or will be) persisted.
|
|
type AuditOutcome int
|
|
|
|
const (
|
|
// AuditCentral means the record goes to the central sink (loki).
|
|
AuditCentral AuditOutcome = iota
|
|
// AuditBuffered means the central sink was unreachable and the record
|
|
// is written to a durable local buffer for later reconciliation
|
|
// (internal/public tier only).
|
|
AuditBuffered
|
|
)
|
|
|
|
// AuditSink is the two-phase, classification-aware audit port (I5, §4.4).
|
|
//
|
|
// Reserve runs BEFORE any write and decides whether the capture can be
|
|
// audited at its effective classification: it returns the outcome to use,
|
|
// or an error to refuse the capture before anything is written
|
|
// (confidential + central sink down → refuse; the all-tiers floor when
|
|
// nothing can record → refuse). Record runs AFTER the writes and persists
|
|
// the final entry per the reserved outcome.
|
|
//
|
|
// Splitting reserve from record is what lets "confidential + sink-down →
|
|
// refuse before any write" be literally true while the record itself
|
|
// (which lists what landed) is necessarily written afterwards.
|
|
type AuditSink interface {
|
|
Reserve(ctx context.Context, level classification.Level) (AuditOutcome, error)
|
|
Record(ctx context.Context, e AuditEntry, outcome AuditOutcome) error
|
|
}
|