feat: uniform capture capability — REST+OAuth2 spine for session insight/ticket capture across all harnesses
#49
Closed
opened 2026-06-22 17:11:14 +00:00 by mathias
·
8 comments
No Branch/Tag Specified
main
fix/pipeline-writes-158
fix/edit-path-io-192
fix/create-regular-188
fix/guard-special-179
fix/linkcheck-keeps-write-178
fix/write-dir-redirect-173
fix/tunnel-detect-174
fix/pipeline-root-157
w3/154-mcp-auth-wiring
fix/tunnel-traversal
test/in-brain-targets-171
fix/brain-scan-root-157
fix/graph-wiki-root-157
fix/vectorstore-root-157
fix/notes-root-157
fix/search-root-157
fix/brainwide-root-157
fix/raw-symlinks-153
feat/symlink-tripwire-157
fix/rest-auth-149
fix/123-brain-edit-envelope-check
feat/106-page-aware-extraction
fix/105-source-drift-on-main
fix/104-surface-merge-conflicts-on-main
agent/b1-compile-on-ingest
agent/104-surface-merge-conflicts
agent/105-source-drift
agent/107-loud-unconfigured-alerting
feat/brain-read-edit
fix/capture-i1-refusal-names-target
renovate/configure
feat/consolidate-single-harness
icebox/cmd-routing
feat/brain-pending-promote
fix/close-session-classification-refresh
fix/capture-summarywriter
v0.24.0
v0.23.1
v0.23.0
v0.22.0
v0.21.1
v0.21.0
v0.20.1
v0.20.0
v0.19.1
v0.19.0
v0.18.4
v0.18.3
v0.18.2
v0.18.1
v0.18.0
v0.17.0
v0.16.1
v0.16.0
v0.15.0
v0.14.0
v0.13.2
v0.13.1
v0.13.0
v0.12.1
v0.12.0
v0.11.2
icebox/cmd-routing-2026-07-01
v0.11.1
v0.11.0
v0.10.0
v0.9.0
v0.8.0
v0.7.0
v0.6.0
v0.5.0
v0.4.1
v0.4.0
v0.3.0
v0.2.0
v0.1.0
Labels
Clear labels
agent/ready
blocked/client
blocked/supplier
blocked/upstream
risk/high
risk/low
risk/medium
size/l
size/m
size/s
size/xs
status:dispatch
status:dispatched
status:review
status:reviewing
type/chore
type/decision
type/docs
type/feat
type/fix
type/ops
type/research
type/test
Meets the agent-ready contract; authorizes unattended execution
Waiting on the client
Waiting on a supplier
Waiting on an upstream project or vendor
Irreversible, security-relevant, or control plane
Reversible, local, test-covered
Touches shared state, deploys or data
Too big - split it
Under two days
Under half a day
Under an hour
Maintenance, no behaviour change
Human decision required; never agent/ready
Documentation only
New capability
Corrects broken behaviour
Infrastructure, deploy, runtime
Investigate; output is a finding, not code
Test coverage only
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: mathias/hyperguild#49
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Intent
One uniform capability for capturing a session's valuable output — insights → brain, action items → Gitea tickets, optional summary → ai-sessions — callable identically from every environment Mathias works in: claude.ai (Chat/Cowork/Code/Design), Claude Code CLI (flamingo/koala/iguana), Crush, Pi, LLM Council (Open WebUI), Agentsquad executor/reviewer, or any future stack.
The goal is a single "command" with the same core logic everywhere and acceptable per-harness quirks only in how the args are assembled, never in what persistence does.
Architecture (decided 2026-06-22)
REST + OAuth2 is the uniform spine. SKILL is the trigger/harvest veneer. MCP is optional sugar. Rationale: REST over authed HTTP is reachable from literally every environment (superset of MCP coverage), stateless, robust, and reuses the existing Authentik/JWT auth. MCP coverage is uneven (great on claude.ai/Crush/Open WebUI, iffy headless) and flakier. So MCP is a convenience wrapper, never the only door.
Layering follows Clean Architecture / idiomatic Go / PragProg-DRY:
CaptureService.Capture(ctx, CaptureInput) (CaptureReceipt, error)in a newingestion/internal/capture/package. Depends only on ports. Owns: validation, the brain write/supersede + read-after-write discipline, ticket routing, summary write, partial-failure receipt assembly.BrainStore—Write,Update,Get(the #45 verbs).IssueTracker—CreateIssue,CloseIssue,CommentIssue.SummaryWriter—WriteFile(repo, path, content).POST /capture(REST, primary) and later an MCPcapturetool — both ~10-line forwarders to the same use-case.Insight,Ticket,Summary,CaptureContext— plain structs.Two ground-truth findings that scope this honestly
Read of
ingestion/internal/api/handler.go+ingestion/internal/mcp/:WriteNote, pipeline, search) and knows nothing about Gitea. TheIssueTrackerport is a new outbound dependency the brain server must gain — not a wrap of existing code. Decide deliberately: brain server reaches out to Gitea directly (new HTTP client + token), OR capture lives in a small new service that composes brain + gitea. (Filing leans: add the client to the brain server, injected, behind theIssueTrackerinterface so it's swappable/testable.)brain_update/brain_getlive in the MCP layer, not the REST/api layer. Only/writeis exposed over REST today (WriteNoteis a clean free function; update/get are MCP-only). The capture use-case needs update/get reachable from REST too. The right move is to extract the brain write/update/get logic into the sharedBrainStoreimplementation that both the MCP handlers and the new REST/capture adapter call — i.e. a small refactor lifting #45's logic out ofmcp/into a layer both adapters share. This is the Clean-Arch payoff but it is real work, not a wrapper.POST /capturecontractAuth: Bearer OAuth2 JWT (reuse existing middleware).
Behaviour (use-case, server-side, once):
Updateifsupersede_slugelseWrite; capture{id, content_hash}. The #45 read-after-write + batch staleness discipline live HERE so no client ever has to know the rule (the DRY win).IssueTracker; owner alwaysmathias.ai-sessionsatsummaries/<harness>/<YYYY-MM>/<date>-<slug>-<ref8>.md, stampingfidelityin frontmatter. Collision rule generalizes: richer fidelity supersedes thinner for the samesession_ref(transcript-parse > live-capture).Failure semantics: best-effort + partial receipt (decided). If a ticket files but a brain write fails, do NOT roll back — return exactly what landed. The server owns partial-failure handling once; no client gets rollback logic.
HTTP: 200 if all ok, 207-style body when partial (all-failed → 4xx/5xx as appropriate).
dry_run: truevalidates + returns the would-be receipt, writes nothing (mirror existing/ingestdry-run).Acceptance criteria (REST + use-case phase — this issue)
ingestion/internal/capture/package withCaptureService+ ports (BrainStore,IssueTracker,SummaryWriter) + entitiesBrainStoreimpl callable from BOTH the MCP handlers and capture (refactor #45's logic out ofmcp/-only)IssueTrackerclient added to the brain server, injected behind the interface, token via env (no secret in argv/logs — see AGENTS.md secret-handling)POST /captureadapter: JWT-authed, decode → use-case → encode, thinok/errorfidelitystamped into summary frontmatter;summaries/<harness>/...pathdry_runpath writes nothing, returns would-be receipttask checkgreen; routes registered; secret-handling respectedFollow-ups (separate issues, after this ships)
capturetool — thin forwarder toCaptureService, for MCP-native clients that prefer it.skills/close-session/SKILL.md) becomes the claude.ai trigger/harvest layer that POSTs/capture; thin equivalents (or direct programmed calls) for Crush/Pi.ai-sessionsextractors) vs chat-memory-reconstruct vs agent-runlog; each assembles/captureargs at its own fidelity.Related
brain_update/brain_get(the verbs this reuses; needs the extract refactor)captureis the act-named verbskills/close-session/SKILL.md— becomes the claude.ai veneer over thisextract_*_sessions.py— the harvest adapters for CLI/Crush/PiGrounded against architecture + regulatory model — design must change before build
Consulted the brain and Gitea architecture record. Findings:
The canonical invariants now exist on main. The sovereign-AI architecture doc set (I1–I5) was sitting un-merged on a branch since 2026-06-05; canonized via infra PR #149. Current-state refresh tracked as infra #150.
This issue's "centralized capture service" framing is in tension with a decided invariant. Brain
wiki/homelab/decisions/no-centralized-cross-harness-observer-2026-06-17explicitly rejected a standing cross-harness observer ("high-degree node — exactly the concentration Zero Trust / sovereign-containment is suspicious of") and endorsed the retro-skill specifically as distributed, per-harness, no shared plumbing. A central/captureservice with cross-harness brain+gitea write reach is closer to the rejected shape than the endorsed one. Per I2 (deliberate acceptance) it fails the admissibility test as written — it opens an undocumented acceptance.Resolution (now in the spec): capture is a shared use-case/library invoked per-harness against the caller's own credentials (distributed consolidation, admissible without a ledger entry). A central authenticated relay is a fallback only for harnesses that can't run the library (Crush/Pi/headless), holds no standing visibility, retains nothing beyond the audit log — and if deployed, must be entered in
infra/docs/security-baseline.mdbefore it ships.New acceptance gates this issue did not have:
k3s/apps/**(no untracked runtime).Spec committed:
specs/capture-bdd-spec.md— use-case form + Gherkin scenarios (happy path, sovereignty refusal, supersession/staleness from #45/#47, fail-closed validation, partial-failure receipt, dry-run, fidelity supersession) + the invariant-obligations table.Four open questions block the build (in the spec §4). The highest-risk: how is
classificationset and trusted? The whole I1 gate is only as strong as the classification it reads — a harness self-declaring "internal" to bypass it is an attack surface. Resolve before dispatch.No BDD/use-case specs existed anywhere before this — this is the first. Worth noting as a process gap for privileged paths generally.
This issue should NOT be dispatched until §4 open questions are resolved and the distributed-vs-relay decision + (if relay) the ledger entry are settled.
§4 open questions resolved — decisions binding, scope updated
Spec §4 resolved (commit
b7a2cc5,specs/capture-bdd-spec.md):context.harnessis descriptive-only, never a gate input (a control keyed on an attacker-suppliable value is not a control).security-baseline.mdledger entry is v1 work, not a follow-up.Scope changes these decisions force on #49
New v1 prerequisites (were not in the original AC):
public/internal/confidentialor similar) + a per-wing / per-repo classification tag the server can read. The I1 gate is not load-bearing until this exists. This is its own chunk of work — likely a sub-issue.Net: #49 is bigger than the original framing. Recommend splitting into sub-issues: (a) classification taxonomy + tagging, (b) the CaptureService use-case + ports + BrainStore extraction from #45's mcp-only logic, (c) REST/OAuth2 adapter + server-derived origin, (d) audit buffer + reconcile, (e) the I2 ledger entry + relay. (b) depends on (a) for the gate; (c)/(e) depend on (b).
Still not dispatch-ready — but now it's blocked on build sequencing/sub-issue breakdown rather than open design questions. The design is settled and BDD-specced.
Q4 revised — classification-aware audit degradation
Reconsidered Q4 (was: global degrade-and-warn + buffer). Now inherits from the Q1 classification spine:
Spec commit
2a595b5. This simplifies the build relative to the prior Q4: the buffer + reconcile machinery only needs to handle internal/public tier, and the confidential path is a plain refuse. It also collapses Q4 into the same classification logic as I1, so there's one sensitivity model, not a separate availability policy.Net effect on the AC delta from the previous comment: the "durable local audit buffer + reconciliation" item is now scoped to internal/public tier only — confidential needs no buffer path. Everything else from that comment stands.
Design is fully settled and BDD-specced. Remaining block is sub-issue breakdown for build sequencing, not design.
Sub-issue breakdown (2026-06-22) — #49 is now a tracking epic
Split into six sub-issues. Dependency graph (two roots converging):
CaptureServiceuse-case +BrainStoreextraction + ports/entitiesIssueTrackerclient (new outbound dep)POST /captureREST + OAuth2 + I1 gate (server-derived origin)Critical path: #50/#51 (parallel) → #52 → #53 → #54 → #55.
Two milestones worth naming:
Start points: #50 and #51 have no dependencies and can begin in parallel. #51 is the biggest chunk (the BrainStore extraction + use-case). Either is a clean first dispatch to koala-Claude.
Design is fully settled (spec
specs/capture-bdd-spec.md, §4 decisions binding). This epic now tracks build sequencing only.Epic complete — capture is uniform across all harnesses
All six sub-issues shipped and merged; tagged v0.11.0.
CaptureServiceuse-case + ports +BrainStoreextraction (one impl with MCP)IssueTracker(owner forced mathias; token never logged)POST /captureREST + OAuth2 + I1 sovereignty gate (server-derived origin)capturerelay tool + I2 ledger + I3 deployArchitecture as decided: REST + OAuth2 is the uniform spine; the use-case logic is written once (
CaptureService) and shared by the REST adapter and the MCP relay tool — distributed-library form for in-process harnesses (CLI/Agentsquad), the thin MCP relay for non-library ones (claude.ai/Crush/Pi/LLM Council) via the existing/mcpconnector.Invariants honoured: I1 (server-derived origin, confidential-through-us-nexus refused) · I2 (relay concentration accepted in
infra/docs/security-baseline.md, merged before relay code) · I3 (env/secret via GitOps, Flux-reconciled) · I5 (request-level audit, classification-aware: confidential fails closed, internal/public degrades + reconciles).Brain (reusable learnings from the build):
decisions/capture-classification-taxonomydecisions/gate-on-server-derived-signals-fail-safedecisions/two-phase-reserve-record-audit-gatefailures/mcp-bearer-middleware-discards-principalFollow-ups not in this epic (per #49's own list): SKILL veneer over the close-session flow; per-harness token provisioning for Crush/Pi/LLM Council (claude.ai done via the existing connector); harvest adapters (transcript-parse vs chat-reconstruct). File as new issues when picked up.
Closing the epic.
As-built report committed
specs/capture-implementation-report.md(commit06e21c0) — the durable record of what shipped, companion tospecs/capture-bdd-spec.md:CaptureService→ 5 ports)BRAIN_GITEA_TOKEN,BRAIN_LOKI_URL,BRAIN_CAPTURE_SOVEREIGN_PRINCIPALS, …)Onboarding + audit trail is now self-contained in the repo.
Post-ship fixes from the first claude.ai dogfood (#66 + #67) — both merged
The 2026-06-23 live dogfood surfaced two operability gaps; both fixed.
classification.yamlwritten at brain root — homelab wings/repos taggedinternal(was: everything but hyperguild/homelab fail-safed to confidential → claude.ai capture refused)SummaryWriterwired (was: nil →"no summary writer configured")#67: tagged
internal— wingshyperguild/homelab; reposbrain,ai-sessions,infra,hyperguild,homelab,tapir,agentsquad,jepa-fx-risk,swedsl.client-*and anything untagged stay confidential (fail-safe intact). Loaded at startup → picked up on the next pod roll.#66: reused the existing
BRAIN_GITEA_TOKEN(no new secret, no manifest change). A live token-scope probe onmathias/ai-sessionsconfirmedpush:true(contents:write) and caught a real bug: gitea's contents API creates via POST, updates via PUT —WriteFilealways PUT'd → 422 on new files. Fixed (POST create / PUT-with-sha update); the httptest mock had the same wrong assumption.Validated under fire (worth noting): the partial-failure receipt did exactly its job — insights + ticket landed, summary failed honestly with a per-item error naming the component, no rollback. The best-effort design held.
Remaining: Flux rolls the pod (loads classification.yaml + summary writer) → re-dogfood from claude.ai with a
summaryblock → expect allok=true,effective_classification: internal. Both STOP points were resolved (map approved; token verified by probe, not assumed).Brain learning:
wiki/hyperguild/failures/mcp-bearer-middleware-discards-principal(from #55) + the dogfood finding note already filed.Verified live (2026-06-24)
ingestion re-rolled (the 15:24Z pod predated the
brain-syncof classification.yaml). Dry-run capture probe touchingai-sessionsnow returnseffective_classification: internal(wasconfidentialpre-roll). I1 gate no longer refuses claude.ai captures touching homelab repos; SummaryWriter wired + token contents:write confirmed. #66 + #67 done and confirmed in prod.