Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Quipu's thesis: **start strict, use agents to bear the cost of strictness.**

- **Graph projection** — materialize subgraphs into petgraph for centrality, connected components, shortest path algorithms.
- **Federation** — `GraphProvider` trait for multi-source queries. Query local and remote Quipu instances in a single operation.
- **Four interfaces** — Rust crate (embed), CLI (`quipu`), REST API (`quipu-server`), and built-in web UI with embeddable web components. Plus 22 MCP tools for agent integration (23 with the `owl` feature).
- **Four interfaces** — Rust crate (embed), CLI (`quipu`), REST API (`quipu-server`), and built-in web UI with embeddable web components. Plus 25 MCP tools for agent integration (26 with the `owl` feature).
- **"SQLite energy"** — single process, no server required, inspect with `sqlite3`, back up with `cp`.
- **Automated releases** — release-plz bumps versions from conventional commits, generates changelogs via git-cliff, and creates GitHub releases. CI runs fmt, clippy, tests, and markdown lint on every push.

Expand Down Expand Up @@ -242,7 +242,7 @@ The reasoner adds forward-chaining inference over the EAVT fact log:
Quipu is designed as a [Bobbin](https://github.com/scbrown/bobbin) subsystem.
Bobbin holds the thread (code context); Quipu ties knots of structured meaning into it.

When running as a Bobbin subsystem, agents get 22 MCP tools (23 with the
When running as a Bobbin subsystem, agents get 25 MCP tools (26 with the
`owl` feature). The two most
commonly used for knowledge-aware context:

Expand Down
22 changes: 20 additions & 2 deletions docs/book/src/reference/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Quipu exposes its API as MCP (Model Context Protocol) tools for agent
integration. These tools are available when Quipu runs as a Bobbin subsystem
or standalone MCP server.

The registry (`tool_definitions()`) exposes **24 tools** in a default build, or
**25** when built with the `owl` feature (which adds `quipu_load_ontology`).
The registry (`tool_definitions()`) exposes **25 tools** in a default build, or
**26** when built with the `owl` feature (which adds `quipu_load_ontology`).

## Tool Reference

Expand Down Expand Up @@ -87,6 +87,24 @@ Retract facts for an entity.
| `timestamp` | No | Retraction timestamp |
| `actor` | No | Who is retracting |

### `quipu_retract_episode`

Episode-scoped **logical** retraction (`POST /episode/retract`). Retracts every
currently-active fact an episode's ingest contributed (activity node, entities,
edges, reified statements) by closing `valid_to` — logical, not physical, so
time-travel history is preserved. Entities and other episodes' facts (even about
shared IRIs) are untouched. Idempotent.

| Parameter | Required | Description |
|-----------|----------|-------------|
| `episode` | Yes | Episode name to retract (aliases: `episode_id`, `name`) |
| `timestamp` | No | Retraction timestamp |
| `actor` | No | Who is retracting |

Retraction is a more sensitive write than assertion. The endpoint honours
read-only mode and bearer auth today; when per-principal scopes (hq-azs) and
crew identity (hq-otm) land it should require an authorized principal.

### `quipu_episode`

Ingest structured agent knowledge as an episode.
Expand Down
33 changes: 33 additions & 0 deletions docs/book/src/reference/rest-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,39 @@ curl -s localhost:3030/retract -X POST \

Optional: `predicate` (only retract matching), `timestamp`, `actor`.

### `POST /episode/retract`

Episode-scoped **logical** retraction. Retracts every currently-active fact an
episode's ingest contributed — its activity node, generated entities, the bare
relationship triples (edges), and any reified confidence statements — by closing
their `valid_to` via the bitemporal retract path. Facts are never physically
deleted, so time-travel queries (`/cord`, `/unravel`) still show them.

The retraction unit is the episode's ingest transaction(s), identified by their
`source = "episode:{name}"` tag. Because identical assertions are deduplicated to
a single owning transaction, retracting an episode only removes the facts *that
episode actually wrote* — entities and facts contributed by other episodes (even
about the same shared IRIs) survive untouched. This is the safe way to undo a
specific episode's contributions without SQL surgery on shared entities.

```bash
curl -s localhost:3030/episode/retract -X POST \
-H "Content-Type: application/json" \
-d '{"episode": "goldblum-deploy-verify-032"}'
```

Aliases for `episode`: `episode_id`, `name`. Optional: `timestamp`, `actor`.
**Idempotent** — retracting an already-retracted or unknown episode returns
`{"retracted": 0}` and changes nothing. Response includes `tx_id`, `retracted`
(count), and `statements` (the retracted facts).

> **Auth (hq-azs / hq-otm).** Retraction is a write — and a *more* sensitive one
> than assertion, since it removes facts from current views. The endpoint is in
> `http_auth::WRITE_ENDPOINTS`, so it already honours read-only mode and the
> bearer token like every other write. When per-principal scopes (hq-azs) and
> crew identity (hq-otm) land, retraction should be gated to an authorized
> principal, not merely the same token that permits assertion.

### `POST /shapes`

Manage persistent SHACL shapes.
Expand Down
124 changes: 124 additions & 0 deletions docs/design/episode-retraction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Episode-Scoped Logical Retraction

> Created: 2026-06-29
> Status: IMPLEMENTED (aegis-hxb)
> Related: [vision.md](./vision.md), [../book/src/architecture/episodes.md](../book/src/architecture/episodes.md)

## One-Line

Retract everything a single episode's ingest contributed — and nothing else — by
closing the bitemporal `valid_to` of exactly the facts that episode's transaction
wrote, surfacing the store's existing internal retraction path over HTTP.

## The Motivating Problem

Before this, Quipu writes were assert-only: `/episode` and `/knot` add facts;
`/cord` / `/unravel` are read-only time-travel. The bitemporal store supported
retraction internally (`op=Retract`, `valid_to`) but nothing surfaced it. So
removing a bad or test-only episode meant hand-surgery on SQLite.

That is dangerous because episodes reuse **real, shared entity IRIs**. A test
episode that touched `quipu-server` and `kota` cannot be undone by deleting all
triples about those entities — that would destroy legitimate graph data.

## The Unit: the Episode's Transaction

Every episode ingest goes through **one** `Store::transact` call stamped
`source = "episode:{name}"` (`episode::ingest_episode` → `rdf::ingest_rdf`).
That transaction source is the complete, precise provenance handle:

- **Complete** — it covers the episode activity node, generated entities, the
bare relationship triples (edges), and reified confidence statements. By
contrast `prov:wasGeneratedBy` only links *entity nodes*, so it would miss
edges and reifications.
- **Precise** — idempotent assertion (`transact` skips a duplicate active
`(e, a, v)`) means each active fact has exactly **one** owning transaction.

So "retract episode X" = close every currently-active asserted fact whose owning
transaction carried `source = "episode:X"`.

### Why this is shared-IRI-safe

Retracting episode X closes only the facts X's transaction actually wrote. A fact
about a shared entity (`quipu-server`, `kota`) that was first asserted by a real
episode keeps that real episode as its owner, so it survives. Re-ingests of the
same episode create multiple transactions that all share the source tag, so every
one of the episode's currently-active contributions is caught.

## Mechanism: Logical, Not Physical

`Store::retract_episode` builds `Op::Retract` datums for the in-scope facts and
commits them through the normal `transact` path. This sets `valid_to` on the
original assertions and records retract rows — it never deletes anything. So:

- The facts drop out of **current** queries (`current_facts`, `/search`, SPARQL
at `valid_now`).
- Time-travel (`/cord`, `/unravel`, `facts_as_of`, `entity_history`) still shows
them, now closed.

Idempotent: a second retraction finds no active facts and is a no-op
(`tx_id == NOOP_TX`, `retracted == 0`). Unknown episodes are likewise no-ops.

## Surface

- Store: `Store::retract_episode(name, timestamp, actor) -> (tx_id, Vec<Fact>)`.
- Tool: `quipu_retract_episode` (`tool_retract_episode`).
- HTTP: `POST /episode/retract` — body `{ "episode": "<name>" }` (aliases
`episode_id`, `name`; optional `timestamp`, `actor`).

## Authorization (hq-azs / hq-otm)

Retraction is a write, and a **more sensitive** one than assertion: it removes
facts from current views. `/episode/retract` is registered in
`http_auth::WRITE_ENDPOINTS`, so today it honours read-only mode and the bearer
token exactly like every other write — under the LAN-trusted default (no token)
it is open like the other writes.

**Requirement for when auth lands:** once per-principal scopes (hq-azs) and crew
identity (hq-otm) are in place, retraction should be gated to an *authorized
principal* — a distinct, higher-trust scope — not merely the same bearer token
that permits assertion. The current single-token model cannot express that
distinction; the gate must be tightened when the identity layer exists.

## First Use: Prune the Goldblum Deploy-Verification Episodes

The first production use of this endpoint is to clean up the bounded,
provenance-marked test episodes left on the live ontology by the aegis-7ui
deploy verification:

- `goldblum-deploy-verify-032`
- `goldblum-confidence-verify-032`
- `goldblum-final-verify-032`
- (plus any dearing co-verify / ian tx341 test episodes, if present)

> **Ownership:** this cleanup runs against the live Quipu store on **kota**
> (`/var/lib/quipu/quipu.db`). It is a **separate goldblum deploy step** — it
> requires the new `quipu-server` binary to be deployed there first. The
> implementing polecat does **not** touch the live store.

### Runbook (run by goldblum after the binary is deployed to kota)

```bash
# 1. Confirm a test episode's facts are currently live (expect rows).
curl -s http://quipu.svc/query -X POST -H 'Content-Type: application/json' \
-d '{"query":"SELECT ?s ?p ?o WHERE { ?s ?p ?o . <http://aegis.gastown.local/ontology/episode_goldblum-deploy-verify-032> ?p2 ?o2 } LIMIT 5"}'

# 2. Retract each test episode (idempotent; safe to re-run).
for ep in goldblum-deploy-verify-032 goldblum-confidence-verify-032 goldblum-final-verify-032; do
curl -s http://quipu.svc/episode/retract -X POST -H 'Content-Type: application/json' \
-d "{\"episode\":\"$ep\",\"actor\":\"goldblum\"}"
echo
done

# 3. Verify the test facts are gone from CURRENT queries (expect 0 rows / ASK false),
# e.g. the deploy-test edge:
curl -s http://quipu.svc/query -X POST -H 'Content-Type: application/json' \
-d '{"query":"ASK { <http://aegis.gastown.local/ontology/quipu-server> <http://aegis.gastown.local/ontology/running_version_on> ?v }"}'

# 4. Confirm real entities survive (expect the real facts intact).
curl -s http://quipu.svc/query -X POST -H 'Content-Type: application/json' \
-d '{"query":"SELECT ?p ?o WHERE { <http://aegis.gastown.local/ontology/quipu-server> ?p ?o }"}'
```

History is preserved: the retracted test facts remain visible via `/cord`
time-travel, now closed — traceable, not erased.
1 change: 1 addition & 0 deletions src/http_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub const WRITE_ENDPOINTS: &[&str] = &[
"/episode",
"/episodes/complete",
"/retract",
"/episode/retract",
"/shapes",
"/impact",
"/propose",
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ pub use mcp::proposal::{
pub use mcp::resolution::tool_resolve_entity;
pub use mcp::search::{tool_search_facts, tool_search_nodes};
pub use mcp::tools::{
tool_cord, tool_episode, tool_hybrid_search, tool_retract, tool_search, tool_shapes,
tool_unravel, tool_validate,
tool_cord, tool_episode, tool_hybrid_search, tool_retract, tool_retract_episode, tool_search,
tool_shapes, tool_unravel, tool_validate,
};
pub use mcp::{tool_definitions, tool_knot, tool_query, value_to_json};
#[cfg(feature = "lancedb")]
Expand Down
13 changes: 13 additions & 0 deletions src/mcp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,19 @@ pub fn tool_definitions() -> Vec<JsonValue> {
"required": ["entity"]
}
}),
serde_json::json!({
"name": "quipu_retract_episode",
"description": "Episode-scoped logical retraction: retract all currently-active facts an episode's ingest contributed (activity node, entities, edges, reified statements), via the bitemporal valid_to close path. Logical, not physical — time-travel history is preserved. Entities and other episodes' facts are untouched. Idempotent.",
"inputSchema": {
"type": "object",
"properties": {
"episode": { "type": "string", "description": "Episode name/identifier to retract (aliases: episode_id, name)" },
"timestamp": { "type": "string", "description": "ISO-8601 timestamp for the retraction" },
"actor": { "type": "string", "description": "Who is performing the retraction" }
},
"required": ["episode"]
}
}),
serde_json::json!({
"name": "quipu_episode",
"description": "Ingest structured knowledge from an agent episode (nodes + edges)",
Expand Down
Loading
Loading