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
13 changes: 13 additions & 0 deletions .agents/artifacts/TECHNICAL_DEBT-wave27.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# TECHNICAL_DEBT — Wave 27 audit (2026-11-09)

| # | Finding | Location | Fix |
|---|---|---|---|
| W27-1 | Attestation chain is truncatable; no signed head/seq; verify is TOFU | `src/sync/attestation.ts`, `src/cli/attest.ts` | Signed sequence + terminal head, trusted key pinning |
| W27-2 | No enclave key rotation/migration; single store-wide key | `src/store/enclaves.ts` | `key_id` rotation with lazy re-encrypt (key_id already in AAD) |
| W27-3 | Semantic merge can be poisoned by self-asserted supersession/content inclusion | `src/quality/branch_merge.ts` | Corroborated lineage; block auto-resolve for constraints/reproducible verification |
| W27-4 | T-437 `scope: client` is not wired into MemoryScope/schema | `src/types.ts`, `schema.json` | Add client scope or reword the ticket |
| W27-5 | Overlay `.merged`/`.dropped` archives accumulate; no GC/listing | `src/store/branch.ts` | Retention sweep + `overlay archives` |
| W27-6 | Branch identity can fall back to `store.dir` when CLI omits `--branch` | `src/store/branch.ts` | Thread `ctx.root` through store-layer creation |
| W27-7 | No MCP surfaces for enclave/attest/overlay | `src/mcp.ts` | Read-only MCP tools with explicit project/client scope |
| W27-8 | Enclave put is read-modify-write without a lock; O(n²) | `src/store/enclaves.ts` | `withStoreLock` + batched put |
| W27-9 | Overlay plaintext archives at 0600 but no size caps | `src/store/branch.ts` | Entry/size caps |
7 changes: 7 additions & 0 deletions .agents/artifacts/gauntlet-wave27/ACCEPTANCE_PACKET.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# ACCEPTANCE PACKET — Wave 27 hardening

**Status: CAPPED.** Code review and muse-security both rejected the first cut; blockers were fixed and re-gated. No second critic round.

Receipts: 1521/0 tests; tsc clean; build 434 modules; secret scan clean. Crypto: GCM AAD + key id, Ed25519 domain-separated signatures, strict tenant/branch names, 0600/0700 files, overlay merge through the three-way resolver with audit.

Residual: `TECHNICAL_DEBT-wave27.md` (chain truncation/TOFU, key rotation, semantic merge poisoning, scope:client not wired, overlay GC, branch identity from store.dir).
36 changes: 36 additions & 0 deletions .agents/artifacts/gauntlet-wave27/ITERATION_LEDGER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# ITERATION LEDGER — Wave 27 (2026-11-09)

## Critics

- **code-review audit / gauntlet**: REJECT, 4.5/10 (Correctness 4, Diff 6, Edge 4, Architecture 4).
- **muse-security audit**: 8 High, 9 Medium, 0 Critical; headline claims overstated.

## Fixed in this round

| Finding | Fix |
|---|---|
| Enclave `safeName` collisions | Strict `[A-Za-z0-9._-]` validation; reject instead of rewrite |
| Envelope metadata not authenticated | GCM AAD over version/key_id/client/division; request metadata asserted |
| Master-key TOCTOU | `wx` creation + EEXIST re-read; 64-hex env only; `key_id` in envelope/AAD |
| Enclave path traversal (`..`) | Name validation + containment by construction |
| Enclave permissions | 0700 dirs, 0600 envelopes; key 0600 |
| Branch name collisions | Reversible base64url filenames; `meta.branch` identity check |
| Overlay path collision with memory versioning | Moved to `.memory/overlays/` |
| Overlay merge lost updates | Captured `base_entries`; merge now calls `mergeMemories3Way` (T-439 wiring) |
| Overlay merge non-atomic / unaudited | Whole-batch validation + `withStoreLock` + per-entry `overlay_merge` audit + `archiveMemory` for deletions |
| Overlay CLI had no write path | `overlay set` / `overlay rm` + audit |
| Detached HEAD treated as branch "HEAD" | `git symbolic-ref`; null on detached, requires `--branch` |
| `overlay list` ignored `--branch` | Uses resolved branch |
| Attest keygen unsafe default/overwrite | Defaults to `~/.config/musememory/keys` (0700), refuses overwrite unless `--force`, audit event |
| Canonical JSON collisions | Rejects unsafe numbers, non-plain objects; omits undefined keys; domain separator |
| Chain semantics | Empty chain rejected; version/algorithm signed and checked; `verify-chain` CLI |
| merge3 fail-open/manual contract | Exit 1 on conflicts unless `--allow-conflicts`; TTY guard; stderr prompts; 0600 output; secret scan |
| Enclave CLI type/traversal/content | Enum validation, name validation, `--file`/stdin |

## Gate

- `bun test` 1521 pass / 0 fail (240 files)
- `tsc --noEmit` clean; build 434 modules; secret scan clean
- New regression tests: name collisions, AAD relabel, empty/tampered chains, overlay 3-way conflict, CLI fail-closed, overlay write surface.

Status: **CAPPED** — one critic round; residual items in `TECHNICAL_DEBT-wave27.md`.
1 change: 1 addition & 0 deletions .agents/context/current.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ on any blocking failure, so CI runs it. Report:
---

## 🏁 Recently Landed Waves (Verified on `dev`)
- [x] **Wave 27 hardening** (2026-11-09) — audit-REJECT fixes: strict names, GCM AAD/key id, audited overlay merge via T-439, fail-closed merge3, corrected claims. Gate 1521/0; CAPPED.
- [x] **Wave 27 (`T-436`–`T-439`)** (2026-11-09) — branch overlays (#377), encrypted enclaves (#378), packet attestations (#379), three-way merge (#380). Tests 1514/0.
- [x] **Sensitive-read audit + project identity** (2026-11-09) — `read` audit events for `scope: "all"`/untrusted; configured project via env/config/`memory project`.
- [x] **Code-context security boundary** (2026-11-09) — project-scoped by default, untrusted opt-in, read-time redaction/sanitization, bounded inputs; `muse_memory_for_code` accepts project/include_untrusted.
Expand Down
6 changes: 3 additions & 3 deletions .agents/context/ticket-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -618,9 +618,9 @@ Source: Strategic roadmaps, developer experience assets, R17–R20 tri-cognitive
- [x] T-435 · LANDED 2026-11-09 — PR #372: Anchor-Matched Invariant Auto-Recall: Bind exported symbols and route interfaces directly to governing ADRs and architectural invariants · src/retrieval/anchors.ts · F

## Wave 27: R19 Git-Branchable Memories & Multi-Tenant Agency Enclaves
- [x] T-436 · LANDED 2026-11-09 — PR #377: Branchable Memory Overlays (`memory overlay` / `merge3`): Branch-isolated memory layers tracking git feature branch lifecycles · src/store/branch.ts · F
- [x] T-437 · LANDED 2026-11-09 — PR #378: Multi-Tenant Agency Enclaves (`scope: client`): Cryptographically segregated client vaults across Council divisions (Sol, Jasper, Crew, Nexus) · src/store/enclaves.ts · F
- [x] T-438 · LANDED 2026-11-09 — PR #379: Zero-Knowledge Memory Attestation: Cryptographic SHA-256 signing of SyncPackets with tamper-evident audit chains for client handoffs · src/sync/attestation.ts · F
- [x] T-436 · LANDED 2026-11-09 — PR #377: Branchable Memory Overlays (`memory overlay` with `set`/`rm`; merge via T-439): Branch-isolated memory layers tracking git feature branch lifecycles · src/store/branch.ts · F
- [x] T-437 · LANDED 2026-11-09 — PR #378: Multi-Tenant Agency Enclaves: authenticated encrypted client vaults (store-key, at-rest); `scope: client` deferred · src/store/enclaves.ts · F
- [x] T-438 · LANDED 2026-11-09 — PR #379: Signed Packet Attestation: detached Ed25519 over canonical packet hashes with tamper-evident audit chains for client handoffs · src/sync/attestation.ts · F
- [x] T-439 · LANDED 2026-11-09 — PR #380: Semantic Contradiction Merge Resolver: Interactive 3-way semantic conflict resolution on branch merge into `main` · src/quality/branch_merge.ts · F

## Wave 28: R20 Test-Driven Reinforcement & Regression Sentry
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **Wave 27 multi-tenant memory**: branch-scoped overlays (`memory overlay`), AES-256-GCM client enclaves, Ed25519 packet attestations with tamper-evident chains, and a three-way semantic memory merge (`merge3`).
- **Wave 27 multi-tenant memory**: branch-scoped overlays (`memory overlay`, three-way merge with base snapshots), authenticated AES-256-GCM client vaults, domain-separated Ed25519 packet attestations, and a three-way semantic memory merge (`merge3`).
- **Unified file→memory resolver**: pre-flight anchored invariants and PR context now share one project/trust/freshness-aware matcher, ranking and reason tokens.
- **Pre-flight surfaces**: a matcher-scoped `PreToolUse` hook and read-only `muse_preflight` MCP tool expose anchored invariants, blast-radius risk and a token-budgeted pack before edits.
- **Wave 26 associative retrieval**: recursive-CTE associative expansion (`memory recall --associative`), speculative pre-flight context (`memory preflight`) with blast-radius risk scoring, and anchor-matched invariant auto-recall.
Expand Down
6 changes: 3 additions & 3 deletions claims.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"networkMillis": 300
},
"tests": {
"count": 1514,
"assertions": 7245,
"files": 239
"count": 1521,
"assertions": 7273,
"files": 240
}
}
44 changes: 37 additions & 7 deletions src/cli/attest.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { chmodSync, readFileSync, writeFileSync } from "node:fs";
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { requireRoot, usageError } from "./shared.ts";
import type { ParsedArgs } from "./shared.ts";
import { generateAttestationKeypair, signPacket, verifyAttestation, type Attestation } from "../sync/attestation.ts";
import { generateAttestationKeypair, signPacket, verifyAttestation, verifyAttestationChain, type Attestation } from "../sync/attestation.ts";
import { recordAuditEvent } from "../audit.ts";

export async function handleAttestCommand({ positional, flags }: ParsedArgs): Promise<number> {
const ctx = requireRoot(flags);
Expand All @@ -11,12 +13,24 @@ export async function handleAttestCommand({ positional, flags }: ParsedArgs): Pr

switch (sub) {
case "keygen": {
const out = flags["out"] ?? ctx.memoryDir;
const out = flags["out"] ?? join(homedir(), ".config", "musememory", "keys");
mkdirSync(out, { recursive: true, mode: 0o700 });
const keyPath = join(out, "attestation.key");
if (existsSync(keyPath) && flags["force"] === undefined) {
return usageError(`${keyPath} exists; pass --force to rotate (old attestations will not verify)`);
}
const pair = generateAttestationKeypair();
writeFileSync(join(out, "attestation.key"), pair.privateKey, { encoding: "utf8", mode: 0o600 });
writeFileSync(keyPath, pair.privateKey, { encoding: "utf8", mode: 0o600, flag: "w" });
writeFileSync(join(out, "attestation.pub"), pair.publicKey, { encoding: "utf8", mode: 0o644 });
try { chmodSync(join(out, "attestation.key"), 0o600); } catch {}
console.log(`wrote ${join(out, "attestation.key")} and attestation.pub`);
try { chmodSync(keyPath, 0o600); } catch {}
recordAuditEvent(ctx.memoryDir, {
operation: "attest_keygen",
entry_id: "attestation",
actor: "cli",
reason: "attestation_keygen",
details: { path: keyPath, rotated: flags["force"] !== undefined },
});
console.log(`wrote ${keyPath} and attestation.pub`);
return 0;
}
case "sign": {
Expand All @@ -25,6 +39,13 @@ export async function handleAttestCommand({ positional, flags }: ParsedArgs): Pr
if (!packetPath || !keyPath) return usageError("attest sign <packet.json> --key <private.pem>");
const packet = JSON.parse(readFileSync(packetPath, "utf8"));
const attestation = signPacket(packet, readFileSync(keyPath, "utf8"), { signer: flags["signer"] });
recordAuditEvent(ctx.memoryDir, {
operation: "attest_sign",
entry_id: "attestation",
actor: "cli",
reason: "packet_signed",
details: { packet_hash: attestation.packet_hash, signer: attestation.signer },
});
console.log(JSON.stringify(attestation, null, 2));
return 0;
}
Expand All @@ -39,7 +60,16 @@ export async function handleAttestCommand({ positional, flags }: ParsedArgs): Pr
console.log(ok ? "valid" : "invalid");
return ok ? 0 : 1;
}
case "verify-chain": {
const chainPath = positional[1];
const pubPath = flags["pub"];
if (!chainPath || !pubPath) return usageError("attest verify-chain <chain.json> --pub <public.pem>");
const chain = JSON.parse(readFileSync(chainPath, "utf8")) as Attestation[];
const ok = verifyAttestationChain(chain, readFileSync(pubPath, "utf8"));
console.log(ok ? "valid" : "invalid");
return ok ? 0 : 1;
}
default:
return usageError(`unknown attest subcommand '${sub}' (expected keygen | sign | verify)`);
return usageError(`unknown attest subcommand '${sub}' (expected keygen | sign | verify | verify-chain)`);
}
}
47 changes: 43 additions & 4 deletions src/cli/branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import {
listBranchMemories,
loadBranchOverlay,
mergeBranchOverlay,
writeBranchEntry,
deleteBranchEntry,
} from "../store/branch.ts";
import { get } from "../store.ts";

export async function handleOverlayCommand({ positional, flags }: ParsedArgs): Promise<number> {
const ctx = requireRoot(flags);
Expand All @@ -16,12 +19,14 @@ export async function handleOverlayCommand({ positional, flags }: ParsedArgs): P
const branch = positional[1] ?? flags["branch"] ?? currentGitBranch(ctx.root) ?? undefined;
if (!branch && sub !== "list") return usageError("branch requires a branch name or a git checkout");

try {
switch (sub) {
case "list": {
const branchName = currentGitBranch(ctx.root);
const branchName = branch ?? currentGitBranch(ctx.root);
const overlay = branchName ? loadBranchOverlay(ctx.memoryDir, branchName) : null;
console.log(branchName ?? "(detached)");
if (overlay) console.log(`overlay: ${overlay.entries.length} entry(ies), ${overlay.deleted_ids.length} deletion(s)`);
else console.log("overlay: none");
return 0;
}
case "create": {
Expand All @@ -36,9 +41,40 @@ export async function handleOverlayCommand({ positional, flags }: ParsedArgs): P
for (const entry of overlay.entries) console.log(` ${entry.id} ${entry.status} ${entry.title}`);
return 0;
}
case "set": {
const id = positional[2];
const content = flags["content"];
if (!id || !content) return usageError("overlay set <branch> <id> --content <text>");
const base = get(ctx.store, id);
const overlay = loadBranchOverlay(ctx.memoryDir, branch!);
const existing = overlay?.entries.find((entry) => entry.id === id) ?? base;
if (!existing) return usageError(`no memory '${id}'`);
writeBranchEntry(ctx.store, branch!, {
...existing,
content,
title: flags["title"] ?? existing.title,
status: (flags["status"] as typeof existing.status) ?? existing.status,
updated_at: new Date().toISOString(),
});
console.log(`overlay ${branch}: set ${id}`);
return 0;
}
case "rm": {
const id = positional[2];
if (!id) return usageError("overlay rm <branch> <id>");
deleteBranchEntry(ctx.store, branch!, id);
console.log(`overlay ${branch}: removed ${id}`);
return 0;
}
case "merge": {
const report = mergeBranchOverlay(ctx.store, branch!);
console.log(`merged ${report.branch}: ${report.applied.length} applied, ${report.deleted.length} archived`);
const strategy = (flags["strategy"] ?? "semantic") as "ours" | "theirs" | "semantic";
const report = mergeBranchOverlay(ctx.store, branch!, { strategy });
if (report.conflicts.length > 0) {
console.error(`merge blocked: ${report.conflicts.length} conflict(s)`);
for (const conflict of report.conflicts) console.error(` ${conflict.id} (${conflict.reason})`);
return 1;
}
console.log(`merged ${report.branch}: ${report.applied.length} applied, ${report.archived.length} archived`);
return 0;
}
case "drop": {
Expand All @@ -51,6 +87,9 @@ export async function handleOverlayCommand({ positional, flags }: ParsedArgs): P
return 0;
}
default:
return usageError(`unknown overlay subcommand '${sub}' (expected list | create | show | entries | merge | drop)`);
return usageError(`unknown overlay subcommand '${sub}' (expected list | create | show | entries | set | rm | merge | drop)`);
}
} catch (err) {
return usageError(err instanceof Error ? err.message : String(err));
}
}
47 changes: 42 additions & 5 deletions src/cli/enclave.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,86 @@
import { randomUUID } from "node:crypto";
import { readFileSync } from "node:fs";
import { requireRoot, usageError } from "./shared.ts";
import type { ParsedArgs } from "./shared.ts";
import { createEnclave, listEnclaveEntries, putEnclaveEntry } from "../store/enclaves.ts";
import { createEnclave, listEnclaveEntries, putEnclaveEntry, validateEnclaveName } from "../store/enclaves.ts";
import { recordAuditEvent } from "../audit.ts";
import type { MemoryEntry, MemoryType } from "../types.ts";

const MEMORY_TYPES: MemoryType[] = [
"session", "decision", "fix", "failure", "architecture", "operation",
"constraint", "preference", "discovery", "negative", "adr",
];

export async function handleEnclaveCommand({ positional, flags }: ParsedArgs): Promise<number> {
const ctx = requireRoot(flags);
if (!ctx) return 1;
const sub = positional[0];
const client = positional[1];
if (!sub || !client) return usageError("enclave requires <create|list|put> <client>");
const division = flags["division"] ?? "default";
let division: string;
try {
division = validateEnclaveName(flags["division"] ?? "default", "division");
} catch (err) {
return usageError(err instanceof Error ? err.message : String(err));
}

try {
switch (sub) {
case "create":
createEnclave(ctx.memoryDir, client, division);
console.log(`enclave ready: ${client}/${division}`);
return 0;
case "list": {
const entries = listEnclaveEntries(ctx.memoryDir, client, division);
recordAuditEvent(ctx.memoryDir, {
operation: "enclave_read",
entry_id: "enclave",
actor: "cli",
reason: "enclave_list",
details: { client, division, count: entries.length },
});
console.log(`${client}/${division}: ${entries.length} entr${entries.length === 1 ? "y" : "ies"}`);
for (const entry of entries) console.log(` ${entry.id} ${entry.type} ${entry.title}`);
return 0;
}
case "put": {
const content = positional[2];
if (!content) return usageError("enclave put requires <content>");
const file = flags["file"];
let content = positional[2];
if (file) content = readFileSync(file, "utf8");
else if (content === "-") content = readFileSync(0, "utf8");
if (!content) return usageError("enclave put requires <content>, --file <path> or '-' for stdin");
const type = (flags["type"] ?? "discovery") as MemoryType;
if (!MEMORY_TYPES.includes(type)) return usageError(`invalid --type '${type}'`);
const id = `m_${Date.now()}_${randomUUID().slice(0, 8)}`;
const now = new Date().toISOString();
const entry: MemoryEntry = {
id,
title: content.slice(0, 80),
content,
project: client,
type: (flags["type"] as MemoryType) ?? "discovery",
source: "enclave",
type,
status: "confirmed",
created_at: now,
updated_at: now,
tags: [],
} as MemoryEntry;
putEnclaveEntry(ctx.memoryDir, client, entry, division);
recordAuditEvent(ctx.memoryDir, {
operation: "enclave_put",
entry_id: id,
project: client,
actor: "cli",
reason: "enclave_put",
details: { client, division, type },
});
console.log(`stored ${id} in ${client}/${division}`);
return 0;
}
default:
return usageError(`unknown enclave subcommand '${sub}' (expected create | list | put)`);
}
} catch (err) {
return usageError(err instanceof Error ? err.message : String(err));
}
}
Loading
Loading