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
37 changes: 37 additions & 0 deletions .github/workflows/memory-distribution-grant.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: memory-distribution-grant

on:
pull_request:
paths:
- "schemas/memory-distribution-grant.schema.json"
- "examples/memory-distribution/**"
- "scripts/validate_memory_distribution_grant.py"
- "docs/architecture/memory-distribution-grant.md"
- ".github/workflows/memory-distribution-grant.yml"
- "Makefile"
push:
branches:
- main
paths:
- "schemas/memory-distribution-grant.schema.json"
- "examples/memory-distribution/**"
- "scripts/validate_memory_distribution_grant.py"
- "docs/architecture/memory-distribution-grant.md"
- ".github/workflows/memory-distribution-grant.yml"
- "Makefile"
workflow_dispatch:

jobs:
validate-memory-distribution-grant:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install validation dependency
run: python -m pip install jsonschema
- name: Validate Memory Distribution Grant examples
run: make validate-memory-distribution-grant
18 changes: 14 additions & 4 deletions .sourceos/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"MemoryObject",
"MemoryProvenance",
"MemoryLifecycleState",
"MemoryReview"
"MemoryReview",
"MemoryDistributionGrant"
],
"syncEngines": [
{
Expand Down Expand Up @@ -35,7 +36,11 @@
"memory.promoted",
"memory.quarantined",
"memory.revoked",
"memory.expired"
"memory.expired",
"memory.distribution_granted",
"memory.distribution_denied",
"memory.materialized_on_target",
"memory.revocation_propagated"
]
}
],
Expand All @@ -54,15 +59,20 @@
"memory.promoted",
"memory.quarantined",
"memory.revoked",
"memory.expired"
"memory.expired",
"memory.distribution_granted",
"memory.distribution_denied",
"memory.materialized_on_target",
"memory.revocation_propagated"
],
"dangerousSurfaces": [
"memory.promote",
"memory.globalize",
"memory.cross_profile_sync",
"memory.rewrite",
"memory.revoke",
"memory.agent_proposed"
"memory.agent_proposed",
"memory.materialize_on_target"
],
"authorityRepos": [
"SourceOS-Linux/sourceos-spec",
Expand Down
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
PYTHON ?= python

.PHONY: validate-upstreams validate-python validate-deploy-assets validate-agent-learning-proposal validate-scenario-learning-binding validate-governed-learning-lifecycle validate-workspace-recall-promotion validate-channel-provenance-write-gate validate-wallguard-memory-compartment-gate validate-prophet-mesh-scope-mirror validate local-preflight local-up local-smoke local-debug local-down
.PHONY: validate-upstreams validate-python validate-deploy-assets validate-agent-learning-proposal validate-scenario-learning-binding validate-governed-learning-lifecycle validate-workspace-recall-promotion validate-channel-provenance-write-gate validate-wallguard-memory-compartment-gate validate-memory-distribution-grant validate-prophet-mesh-scope-mirror validate local-preflight local-up local-smoke local-debug local-down

validate-upstreams:
$(PYTHON) scripts/validate_upstreams.py third_party/upstreams.lock.yaml
Expand Down Expand Up @@ -33,7 +33,10 @@ validate-channel-provenance-write-gate:
validate-wallguard-memory-compartment-gate:
$(PYTHON) scripts/validate_wallguard_memory_compartment_gate.py

validate: validate-upstreams validate-python validate-deploy-assets validate-wallguard-memory-compartment-gate validate-prophet-mesh-scope-mirror
validate-memory-distribution-grant:
$(PYTHON) scripts/validate_memory_distribution_grant.py

validate: validate-upstreams validate-python validate-deploy-assets validate-wallguard-memory-compartment-gate validate-memory-distribution-grant validate-prophet-mesh-scope-mirror

local-preflight:
bash deploy/local/scripts/preflight-podman-m2.sh
Expand Down
45 changes: 44 additions & 1 deletion adapters/openclaw-memory-mesh/src/edgeMemoryStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ export type EdgeMemoryEntry = {
created_at: string;
updated_at: string;
sync_state: "local_only" | "pending_write" | "synced" | "conflict";
// Distribution-grant fields (see docs/architecture/memory-distribution-grant.md).
// Revocation is enforced at read: a revoked entry is never returned by search(),
// regardless of whether a full sync has run since the tombstone landed.
revoked?: boolean;
revoked_at?: string | null;
};

type EdgeMemoryStoreOptions = {
Expand Down Expand Up @@ -150,8 +155,9 @@ export class EdgeMemoryStore {
];
for (const entry of entries.slice().sort((a, b) => b.updated_at.localeCompare(a.updated_at))) {
lines.push(`## ${entry.memory_class}`);
lines.push(`- content: ${entry.content}`);
lines.push(`- content: ${entry.revoked ? "(revoked — tombstoned)" : entry.content}`);
lines.push(`- sync_state: ${entry.sync_state}`);
lines.push(`- revoked: ${entry.revoked ? `true (${entry.revoked_at ?? "unknown"})` : "false"}`);
lines.push(`- mesh_event_id: ${entry.mesh_event_id ?? "none"}`);
lines.push(`- updated_at: ${entry.updated_at}`);
lines.push("");
Expand All @@ -167,6 +173,12 @@ export class EdgeMemoryStore {
const orderedScopes = buildScopeOrder(scopeOrder);
const hits: MemoryHit[] = [];
for (const entry of entries) {
// Read-enforced revocation: a tombstoned entry must never surface, even if a
// full sync has not yet reconciled it away. This is the edge honoring of the
// MemoryDistributionGrant revocation cursor (enforceAtRead).
if (entry.revoked) {
continue;
}
const scope = scopeNameForEnvelope(envelope, entry.envelope);
if (scope === "none") {
continue;
Expand Down Expand Up @@ -229,4 +241,35 @@ export class EdgeMemoryStore {
await this.writeEntries(entries);
return entry;
}

/**
* Apply a revocation tombstone at the edge as a first-class event, rather than
* waiting for the next full sync to reconcile the entry away. Matches by mesh
* event id or local id. Once tombstoned, the entry is retained (for audit and
* offline "last accepted revision" display) but is filtered out of search().
* Returns the number of entries tombstoned.
*/
async revoke(ref: string): Promise<number> {
if (!this.enabled) {
return 0;
}
const entries = await this.readEntries();
const timestamp = new Date().toISOString();
let tombstoned = 0;
for (const entry of entries) {
if (entry.revoked) {
continue;
}
if (entry.local_id === ref || entry.mesh_event_id === ref) {
entry.revoked = true;
entry.revoked_at = timestamp;
entry.updated_at = timestamp;
tombstoned += 1;
}
}
if (tombstoned > 0) {
await this.writeEntries(entries);
}
return tombstoned;
}
}
117 changes: 117 additions & 0 deletions docs/architecture/memory-distribution-grant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Memory Distribution Grant

## Status

Contract v0.1. Fixture-backed and CI-validated. Runtime honoring begins in the OpenClaw
edge adapter (read-enforced revocation); broader runtime wiring is follow-on work.

## Why this exists

Memory Mesh already governs two decisions:

1. **Scope approval** — may a memory become active in a governed scope? Owned by the
governed-learning lifecycle (`schemas/governed-learning-lifecycle-record.schema.json`):
`observed → proposed → scoped → approved → promoted → revoked`.
2. **Compartment admission** — may a memory cross from one wall/compartment to another
(including into a derived embedding/recall lane)? Owned by WallGuard
(`schemas/wallguard-memory-compartment-gate.schema.json`), which gates
`read_memory`, `write_memory`, `semantic_recall`, `embedding_write`,
`memory_promotion`, and `clean_room_release`, and can `quarantine`.

Neither answers a third, independent question: **may _this specific endpoint_ — a device,
a profile, a surface — receive and materialize a memory that is already approved in scope
and admitted across the wall, and until when?**

A memory can be globally approved and still be too sensitive for a shared kiosk, a
semi-trusted phone, or a profile that should never hold matter-privileged content. Scope
approval is not a distribution right. This contract makes that distinction first-class.

## The three axes (and why we do not collapse them)

| Axis | Question | Owner |
|------|----------|-------|
| Scope approval | Active in a governed scope? | governed-learning-lifecycle |
| Compartment admission | May it cross this wall / enter the derived lane? | WallGuard |
| **Endpoint distribution** | **May _this_ device/profile/surface hold it, until when?** | **MemoryDistributionGrant** |

The grant **chains** the first two rather than re-deciding them. `upstreamAuthority`
carries a `lifecycleRecordRef` (+ `scopeApprovalStatus`) and a `wallDecisionRef`
(+ `wallDecisionOutcome`). The grant contributes exactly one new decision — the endpoint
one — plus the machinery to keep that decision honest over time. This is deliberate: we do
not want a second compartment engine, and we do not want distribution logic leaking into
WallGuard. If WallGuard denied or quarantined, or the scope was revoked, the grant cannot
admit — the validator enforces this.

## What the grant adds

- **Endpoint sensitivity ceiling.** `target.endpointSensitivityCeiling` bounds what an
endpoint may hold. An `admit` whose memory sensitivity exceeds the ceiling is rejected
(`over_sensitive_for_target`). This is the "globally approved, still too sensitive here"
case, encoded.
- **Standing-privilege expiry.** `grantValidity.expiresAt` forces re-authorization. A fresh
scope approval does not silently renew a distribution grant.
- **Content digest.** `memoryRef.contentDigest` (sha256) + `memoryVersion` give the target
tamper-evidence and dedup, and bind the grant to an exact revision.
- **Read-enforced revocation cursor.** `revocationCursor` is a monotonic `watermark` plus
`tombstoneRefs`, with `enforceAtRead: true`. Revocation propagates as a first-class event
(`propagationMode: event_first`), **but correctness does not depend on the event landing**:
the target filters against the cursor at read time, so a revoked item cannot be served
even if the push has not yet arrived. `poll_fallback` covers degraded connectivity.
- **Explicit offline posture.** A disconnected target never claims current global truth. It
either serves the last accepted revision flagged stale
(`serve_last_accepted_with_staleness`, within a `stalenessBudgetSeconds`) or denies until
fresh (`deny_until_fresh`). `sensitive-redacted` memory is pinned to `deny_until_fresh`.
- **Evidence-fabric receipt.** `distributionReceiptRef` points into the shared
Run/Event/Receipt spine rather than a bespoke sync-receipt blob, so distribution is
auditable the same way every other governed surface is.

## Materialization rule

`materialization.verifyGrantBeforeMaterialize` is always true: a target verifies the grant
(decision `admit`, unexpired, cursor not revoked, digest match) **before** materializing.
Only an admitted grant may be materialized, and a materialized grant must carry a receipt.
While an item is quarantined, `quarantineDerivedLaneBlocked` records that the target will
not admit it into a derived graph/index lane another endpoint can query — the lane itself is
gated by WallGuard, not re-implemented here.

## Graph materialization (HellGraph) honors revocation

The derived-lane concern is concrete in this estate: memory is materialized into the managed
**HellGraph** graph (the `workspace_ingestion` CSKG → HellGraph path). A revocation that only
tombstoned the canonical row would leave the revoked knowledge queryable as graph edges. So
revocation propagates into the graph:

- `memoryd` `POST /v1/revoke` tombstones the memory (read-enforced across the in-memory,
SQLite, and Postgres backends — a revoked row is excluded from every subsequent recall,
including externally-indexed vector hits), **and** calls HellGraph `POST /v1/retract` to
supersede the edges derived from that memory.
- **Provenance convention (matters for correctness):** HellGraph edges are keyed by their
originating `workspace-source:{surface}/{slug}` refs (the `workspace_ingestion` CSKG path),
*not* by memory id. So the retract keys on the real provenance: `memoryd` reads the memory's
stored `metadata.provenance_refs` and passes those to retract, merged with any caller-supplied
refs and a canonical `memory://{memory_id}` self-ref. The PKG ingest writeback records those
`workspace-source` refs into the summary memory's metadata precisely so revoking it supersedes
the edges it derived. A memoryd memory with no derived edges retracts nothing (no-op).
- The retract binding graceful-degrades: with `HELLGRAPH_URL` unset it is inert and the local
tombstone still holds, so the service (and tests) run without a live graph.
- A `memory.revocation_propagated` audit event records the graph source refs and retract result.

This closes the loop: recall, the vector index, and the graph materialization all stop serving
a revoked memory, without waiting on a full sync.

## Fixtures

- `grant.admitted.example.json` — trusted device, internal memory within ceiling.
- `grant.over-sensitive-denied.example.json` — sensitive-redacted memory denied to a
semi-trusted shared surface.
- `grant.revoked-propagated.example.json` — scope revoked; cursor advanced with tombstone;
deny + not materialized.
- `grant.offline-stale.example.json` — served last accepted revision, flagged stale, within
budget.

## Related

- `schemas/wallguard-memory-compartment-gate.schema.json` — compartment admission.
- `schemas/governed-learning-lifecycle-record.schema.json` — scope approval + revocation record.
- `contracts/prophet-mesh/prophet-mesh-memory-scope.v0.1.json` — scope-presence mirror.
- Issue #18 (governed memory lifecycle), issue #36 (WallGuard runtime memory gate).
23 changes: 18 additions & 5 deletions docs/architecture/memorymesh-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ We are not building around a managed memory product. We can still choose pragmat

### Functional capabilities

The system provides six core capabilities.
The system provides seven core capabilities.

#### A. Memory write

Expand All @@ -111,19 +111,32 @@ The system:
- optionally expands to backend workers;
- merges, scores, deduplicates, and returns bounded hits.

#### C. Workload configuration
#### C. Memory revocation

A caller revokes a memory by id. The system:

- tombstones the row so it is excluded from every subsequent recall (read-enforced across the
in-memory, SQLite, and Postgres backends, and filtered out of externally-indexed vector hits);
- propagates the revocation into the HellGraph materialization by retracting the derived edges
(`memory://{memory_id}` plus any declared upstream provenance refs), graceful-degrading when no
graph is configured;
- emits a `memory.revocation_propagated` audit event.

See [memory-distribution-grant.md](./memory-distribution-grant.md).

#### D. Workload configuration

A caller or sidecar asks for the compiled config for a workload. This config is derived from attached resources such as `MemoryAttachment`, `GlobalRecallPolicy`, `MemoryPeer`, `ExportPolicy`, and `ConflictPolicy`.

#### D. Config watch
#### E. Config watch

Long-lived processes can subscribe to a config watch stream and react to policy changes without restarts.

#### E. Event inspection
#### F. Event inspection

Operators can inspect recent events for local debugging and audit.

#### F. Adapter mediation
#### G. Adapter mediation

Thin adapters translate native upstream conventions into the canonical envelope and the `memoryd` contract.

Expand Down
6 changes: 5 additions & 1 deletion docs/architecture/openclaw-edge-memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ The current branch now includes:
- adapter configuration for edge-memory enablement and local recall thresholds;
- a file-backed edge-memory mirror with generated `MEMORY.md` projection;
- adapter-side recall merge logic across edge and mesh hits;
- `memoryd` recall ordering that respects compiled `local_first` and `recall_scope_order` more concretely.
- `memoryd` recall ordering that respects compiled `local_first` and `recall_scope_order` more concretely;
- read-enforced revocation: `EdgeMemoryStore.revoke()` tombstones an entry as a first-class
event and `search()` filters revoked entries at read time, so a revoked memory cannot be
served from the edge even before the next full sync (see
[memory-distribution-grant.md](./memory-distribution-grant.md)).

## Current scope model

Expand Down
Loading
Loading