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
28 changes: 28 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,34 @@ demo-P-TPS.1: ## P-TPS.1 (ADR-0044): streaming output-token readout — output-o
demo-P-SKILL.1: ## P-SKILL.1 (ADR-0045): gated skill import — clean .md writes to .omp/skills/, poisoned blocks at the gate
$(BUN) run harness/scripts/demo_pskill1.ts

.PHONY: demo-P-SKILL.4
demo-P-SKILL.4: ## P-SKILL.4 (ADR-0097): the Agent Skill directory - classify roots/trust, fail-closed re-scan locks a flagged skill, remove confined to project/user (immutable .agents refused)
$(BUN) run harness/scripts/demo_pskill4.ts

.PHONY: demo-P-SKILLREG.1
demo-P-SKILLREG.1: ## P-SKILLREG.1 (ADR-0098): the enterprise skills registry READER - Ed25519 verify + fail-closed scan-gate on install; unsigned/untrusted-key/poisoned blocked, clean installs as an untrusted registry row
$(BUN) run harness/scripts/demo_pskillreg1.ts

.PHONY: demo-P-SKILL.5
demo-P-SKILL.5: ## P-SKILL.5 (ADR-0101): Skill Studio - analyze recent work into candidate skills, codify each through the fail-closed gate (clean writes, poisoned blocks); analyze writes nothing
$(BUN) run harness/scripts/demo_pskill5.ts

.PHONY: demo-P-SKILLREG.2
demo-P-SKILLREG.2: ## P-SKILLREG.2 (ADR-0102): the skill publish seam - sign + publish a codified skill to the Local Skills Registry (remote target no-ops fail-safe), then round-trip it back through the reader into a registry row
$(BUN) run harness/scripts/demo_pskillreg2.ts

.PHONY: demo-P-KB.1
demo-P-KB.1: ## P-KB.1 (ADR-0099): the compiled KB - a clean doc compiles into a page graph, a poisoned source is quarantined (never compiled), a poisoned derived page is re-scanned + quarantined (never stored)
$(BUN) run harness/scripts/demo_pkb1.ts

.PHONY: demo-P-KB.2
demo-P-KB.2: ## P-KB.2 (ADR-0100): the hybrid retrieval router (vector | compiled | both, delimited + cited) + the kept-in-sync generator (idempotent re-ingest, contradiction-flagged, prior page retained)
$(BUN) run harness/scripts/demo_pkb2.ts

.PHONY: demo-P-KB.2b
demo-P-KB.2b: ## P-KB.2b (ADR-0099/0100 desktop): the desktop compiled-KB surface - ingest compiles into the process store, retrieve returns cited delimited hits, graph exposes pages+links, poisoned source quarantined
$(BUN) run desktop/scripts/demo_p_kb_1.ts

.PHONY: demo-P-GOAL.9
demo-P-GOAL.9: ## P-GOAL.9 (ADR-0054): /goal After-Action Report — tool calls/LOC/errors/websites graphs + stall guard
$(BUN) run harness/scripts/demo_pgoal9.ts
Expand Down
41 changes: 41 additions & 0 deletions PROGRESS.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -502,8 +502,8 @@ if poisoned, *before* it can ever be embedded or recalled. (Keystone #2 holds fo
- 🎛️ **Manage, don't just list.** Inspect a skill's body + bundled scripts/references read-only, **enable/disable** it, **re-scan** it through the fail-closed gate, and remove imported ones - bundled assets stay immutable.
- 🛡️ **Fail-closed by construction.** A `suspicious`/`quarantined` skill is shown but **cannot be enabled or loaded**; a dead scanner on re-scan means quarantine, never "safe." Skill bodies are delimited *data*, never instructions (keystone #2 holds for skills too).
- 🛠️ **Skill Studio - turn your week into skills (coming soon).** Designed in [ADR-0101](DECISIONS.md) (`P-SKILL.5`), a one-click button that analyzes your **day's or week's** work (sessions, AI-authored edits, loop outcomes) and **drafts Agent Skills** with your most-used model - each one **scanned before it's saved** and **reviewed before it's codified** (a reviewed draft is excellent; an un-reviewed one is worse than none). Codified skills land in your **Local Skills Registry**.
- 🏛️ **Enterprise skills registry (premium, coming soon).** Publish, version, **sign (Cosign + SLSA provenance)**, scan, and distribute skills privately - as portable **OCI artifacts on an S3-compatible backend** that stands up identically on **AWS, Azure, Google Cloud, OCI, IBM Cloud, VMware, Nutanix, NetApp ONTAP, and KVM** via Terraform, including **air-gapped and IL5** government partitions. Install = fetch → **verify signature → scan-gate → install**; an unsigned or flagged skill is blocked. The app ships the read-only registry-reader seam; the hosting runbooks are a separately-licensed add-on.
- 🚀 **Push to where your org already lives (premium, coming soon).** Designed in [ADR-0102](DECISIONS.md) (`P-SKILLREG.2`), a single **publish seam** ships in the core with a default **local** publisher; the remote publishers - enterprise cloud registries (AWS/Azure/GCP/Oracle/IBM) and **custom git** (Enterprise GitLab, GitHub, Azure DevOps) - are a separately-licensed add-on. Every push is **egress-gated** and centrally policy-clamped.
- 🏛️ **Enterprise skills registry - reader seam ships now (`P-SKILLREG.1`).** The source-available app carries the read-only **registry reader**: an install is **fetch → verify (Ed25519 signature vs. your trusted keys) → scan-gate (the same fail-closed gate) → install**, and an **unsigned, signature-mismatched, unconfigured-key, or scan-flagged** skill is **blocked, never written** (keystone #2: an installed registry skill is shown `untrusted`, never auto-promoted to trusted). Installed skills appear in the directory above under a **Registry** source. The hosting side - publish/version/**sign (Cosign + SLSA)**/distribute as portable **OCI artifacts on an S3-compatible backend** that stands up identically on **AWS, Azure, Google Cloud, OCI, IBM Cloud, VMware, Nutanix, NetApp ONTAP, and KVM** via Terraform, incl. **air-gapped and IL5** partitions - is the separately-licensed add-on (server + runbooks are private IP).
- 🚀 **Push to where your org already lives - publish seam ships now (`P-SKILLREG.2`).** A single **`RegistryPublisher`** seam ships in the core with a default **`LocalRegistryPublisher`** (serves your skills as the Local Skills Registry) + a fail-safe `PublishDispatcher` (a dead/missing publisher never throws into a turn; a declared remote with no publisher is a clean no-op). The remote publishers - enterprise cloud OCI registries (AWS/Azure/GCP/Oracle/IBM) and **custom git** (Enterprise GitLab, GitHub, Azure DevOps) - implement the same interface and are a separately-licensed add-on. Publishing establishes **no trust**: the read side still verifies the signature + scan-gates before install; every remote push is **egress-gated** and centrally policy-clamped.

## <img src=".github/assets/icons/builton.svg" width="28" align="top" alt=""> Built on

Expand Down
100 changes: 99 additions & 1 deletion desktop/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,15 @@ function settingsData() {
}
import { emailDomainAllowed, managedAsksageOnly, managedConfig, managedLocks, skipAllowed } from "./managed_config.ts";
import { asksageConfig, listDatasets, listPersonas, monthlyTokens, scanPersona, wrapPersona } from "./asksage.ts";
import { listSkills } from "./skills_data.ts";
import { inspectSkill, listSkills, removeSkill, rescanSkill } from "./skills_data.ts"
import { intelNews } from "./intel_news.ts"; // P-TRIV.3 (ADR-0176): the executive Trivia Wire's news feed
import { installRegistrySkill, type RegistrySkillArtifact } from "./skills_registry.ts"
import { analyzeWork, codifyCandidate, gatherWorkDigest, type SkillCandidate, type StudioWindow } from "./skill_studio.ts"
import { buildSkillArtifact, PublishDispatcher, publishersFor } from "./skill_publish.ts";
import { kbScanner, kbStore } from "./kb_store.ts"
import { ingestDocument } from "../harness/kb/ingest.ts"
import { retrieveKnowledge, type RetrieveMode } from "../harness/kb/retrieve.ts"
import { recordBlock } from "./security_log.ts"
import { importSkill } from "./skills_import.ts";
import { createUserCommand, deleteUserCommand, listUserCommands } from "./user_commands.ts"; // P-CMD.1
import { archiveGoalReport, deleteGoalReport, listResumableLoops, listGoalReports, readGoalReport, restoreGoalReport } from "./goal_memory.ts";
Expand Down Expand Up @@ -1390,6 +1397,97 @@ const server = Bun.serve({
}
return json({ ok: true, data: { results } });
}
// P-SKILL.4 (ADR-0097): the directory's per-skill management menu. All three resolve the skill by
// NAME through omp's own discovery (never a client-supplied path) and are pathWithin-confined:
// inspect — read the SKILL.md body + resource tree as DATA (the renderer delimits it);
// rescan — run the fail-closed gate + record the verdict (dead scanner ⇒ quarantined);
// remove — delete a project/user skill dir; bundled/agents/plugin are refused (immutable).
if (p === "/api/skills/inspect" && req.method === "POST") {
const b = await readBody<{ name?: unknown }>(req);
return json({ ok: true, data: await inspectSkill(String(b.name ?? "")) });
}
if (p === "/api/skills/rescan" && req.method === "POST") {
const b = await readBody<{ name?: unknown }>(req);
return json({ ok: true, data: await rescanSkill(String(b.name ?? "")) });
}
if (p === "/api/skills/remove" && req.method === "POST") {
const b = await readBody<{ name?: unknown }>(req);
return json({ ok: true, data: await removeSkill(String(b.name ?? "")) });
}
// P-SKILLREG.1 (ADR-0098): the enterprise-registry READER seam. A connector (private add-on) POSTs a
// fetched, signed skill artifact; the reader verifies its signature, runs it through the fail-closed
// scan gate, and installs it locally ONLY if both pass \u2014 then it appears as a `registry` directory row.
// Fail-closed: unsigned / bad-signature / scan-flagged / dead-scanner \u2192 blocked, nothing written.
if (p === "/api/skills/registry/install" && req.method === "POST") {
const b = await readBody<{ artifact?: RegistrySkillArtifact }>(req);
if (!b.artifact) return json({ ok: true, data: { ok: false, name: "", installed: false, stage: "validate", reason: "missing artifact" } });
return json({ ok: true, data: await installRegistrySkill(b.artifact) });
}
// P-SKILL.5 (ADR-0101): Skill Studio. `analyze` gathers the user's recent work into a delimited DATA
// digest and asks the most-used model for candidate skills; `draft` codifies ONE reviewed candidate
// through the SAME fail-closed import gate (clean writes to .omp/skills, flagged blocks). Analysis
// uses backend.complete (the model call) with the digest's model; nothing is written at analyze time.
if (p === "/api/skill-studio/analyze" && req.method === "POST") {
const b = await readBody<{ window?: unknown }>(req);
const window: StudioWindow = b.window === "today" ? "today" : "week";
const data = await analyzeWork(window, {
gather: gatherWorkDigest,
complete: (system, user, model) => backend.complete(system, user, model ? { model } : {}),
});
return json({ ok: true, data });
}
if (p === "/api/skill-studio/draft" && req.method === "POST") {
const b = await readBody<{ candidate?: SkillCandidate }>(req);
const c = b.candidate;
if (!c || typeof c.name !== "string" || typeof c.description !== "string" || typeof c.body !== "string") {
return json({ ok: true, data: { ok: false, name: "", reason: "invalid candidate" } });
}
return json({ ok: true, data: await codifyCandidate({ name: c.name, description: c.description, body: c.body }) });
}
// P-KB.2b (ADR-0099/0100 desktop plumbing): the compiled knowledge base surface. ingest = scan the
// source + compile (backend.complete, most-used model) + re-scan each derived page, all fail-closed;
// retrieve = the router (the vector store is not desktop-wired yet, so vector/hybrid return the
// compiled hits) delimited + cited; graph = pages + links for the force-graph view.
if (p === "/api/kb/ingest" && req.method === "POST") {
const b = await readBody<{ sourcePath?: unknown; title?: unknown; text?: unknown }>(req);
const text = String(b.text ?? "");
if (!text.trim()) return json({ ok: true, data: { documentId: "", status: "quarantined", pagesCompiled: 0, pagesQuarantined: 0, links: 0, pageIds: [], blocked: [{ stage: "source", reason: "empty document", trustLabel: "quarantined", findings: 0 }] } });
const model = usageLedger().models[0]?.model;
const result = await ingestDocument({
store: await kbStore(),
scanner: kbScanner(),
complete: (system, user) => backend.complete(system, user, model ? { model } : {}),
sourcePath: String(b.sourcePath ?? "document"),
title: String(b.title ?? "Untitled"),
text,
onBlock: (blk) => recordBlock({ tool: "kb_ingest", severity: "high", findings: String(blk.findings), reason: `KB ${blk.stage} blocked${blk.slug ? ` (${blk.slug})` : ""}: ${blk.reason}` }),
});
return json({ ok: true, data: result });
}
if (p === "/api/kb/retrieve" && req.method === "POST") {
const b = await readBody<{ query?: unknown; mode?: unknown }>(req);
const mode: RetrieveMode = b.mode === "vector" || b.mode === "hybrid" ? b.mode : "compiled";
return json({ ok: true, data: await retrieveKnowledge({ query: String(b.query ?? ""), mode, compiled: { store: await kbStore() } }) });
}
if (p === "/api/kb/graph") {
const s = await kbStore();
return json({ ok: true, data: { pages: await s.listPages(), links: await s.listLinks() } });
}
// P-SKILLREG.2 (ADR-0102): the PUBLISH seam. Reads a codified skill's SKILL.md and publishes it as a
// versioned artifact to the configured targets (fail-safe fan-out). Public ships the LOCAL publisher;
// a declared remote with no registered publisher is a clean no-op. Publishing establishes NO trust:
// the signature + scan gate still run on the READ side (P-SKILLREG.1) before anything installs.
if (p === "/api/skills/publish" && req.method === "POST") {
const b = await readBody<{ name?: unknown; version?: unknown; targets?: unknown }>(req);
const insp = await inspectSkill(String(b.name ?? ""));
if (!insp.ok || !insp.body) return json({ ok: true, data: { ok: false, receipts: [], reason: insp.reason ?? "skill not found or empty" } });
const version = typeof b.version === "string" && b.version ? b.version : "1.0.0";
const targets = Array.isArray(b.targets) ? b.targets.filter((t): t is string => typeof t === "string") : undefined;
const dispatcher = new PublishDispatcher();
dispatcher.setPublishers(publishersFor());
const receipts = await dispatcher.publish(buildSkillArtifact({ name: insp.name, version, content: insp.body }), targets);
return json({ ok: true, data: { ok: receipts.some((r) => r.ok), receipts } });
}
// P-CMD.1 (ADR-0146): user-authored "/" slash commands. GET = list stored commands; POST = create one
// (validate → secret-scan → Unicode-scan, all fail-closed → persist). The delete route removes one.
if (p === "/api/usercommand" && req.method === "GET") return json({ ok: true, data: listUserCommands() });
Expand Down
33 changes: 33 additions & 0 deletions desktop/kb_store.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2026 TechLead 187 LLC
// SPDX-License-Identifier: BUSL-1.1

// desktop/kb_store.test.ts — P-KB.2b: the desktop's compiled-KB handle. Confirms the process-wide store
// opens at the configured path (migrated + empty), is a genuine singleton across calls, and reopens fresh
// after teardown. The compile/gate/retrieve logic itself is covered in harness/kb/*.test.ts.

import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { _resetKbStoreForTest, kbStore, stopKb } from "./kb_store.ts";

describe("kbStore — desktop singleton", () => {
let dir: string;
beforeEach(() => { dir = mkdtempSync(join(tmpdir(), "kb-desktop-")); process.env.LUCID_KB_DB_PATH = join(dir, "kb_graph.duckdb"); _resetKbStoreForTest(); });
afterEach(async () => { await stopKb(); delete process.env.LUCID_KB_DB_PATH; rmSync(dir, { recursive: true, force: true }); });

test("opens at the configured path (migrated + empty) and returns the SAME instance", async () => {
const a = await kbStore();
const b = await kbStore();
expect(a).toBe(b); // one process-wide writer
expect(await a.pageCount()).toBe(0);
});

test("reopens a fresh store after teardown", async () => {
const first = await kbStore();
await stopKb(); // closes + nulls the singleton
const second = await kbStore();
expect(second).not.toBe(first);
expect(await second.pageCount()).toBe(0);
});
});
42 changes: 42 additions & 0 deletions desktop/kb_store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) 2026 TechLead 187 LLC
// SPDX-License-Identifier: BUSL-1.1

// desktop/kb_store.ts — P-KB.2b (ADR-0099/0100 desktop plumbing): the desktop's handle to the compiled
// knowledge base. Opens ONE kb_graph.duckdb (single writer for this process) lazily and hands the
// dev-server routes a shared KbGraphStore + a shared scanner for the gated ingest pipeline.
//
// The compiled KB is a SEPARATE DuckDB file (harness/kb migrations), a sibling to the vector store and to
// agent_obs.duckdb — no write-lock contention. Path is overridable (LUCID_KB_DB_PATH) for demos/tests.
// SECURITY: this module only OPENS the store + the scanner; the fail-closed gating lives in the harness
// ingest pipeline (scan source + re-scan every derived page). A single lazy scanner mirrors skills_import.

import { join } from "node:path";
import { homedir } from "node:os";
import { KbGraphStore } from "../harness/kb/store.ts";
import { ScannerClient } from "../harness/security/scanner_client.ts";

const DEFAULT_KB_DB = join(homedir(), ".omp", "kb_graph.duckdb");

let storeP: Promise<KbGraphStore> | null = null;
/** The process-wide compiled-KB store (opened + migrated once). */
export function kbStore(): Promise<KbGraphStore> {
if (!storeP) storeP = KbGraphStore.open(process.env.LUCID_KB_DB_PATH || DEFAULT_KB_DB);
return storeP;
}

let scanner: ScannerClient | null = null;
/** The shared scanner sidecar for KB ingest (fail-closed: a dead scanner quarantines in the pipeline). */
export function kbScanner(): ScannerClient {
if (!scanner) { scanner = new ScannerClient(); scanner.start(); }
return scanner;
}

/** Tear down the scanner + close the store (demo/test teardown). */
export async function stopKb(): Promise<void> {
try { scanner?.stop(); } catch { /* ignore */ }
scanner = null;
if (storeP) { try { (await storeP).close(); } catch { /* ignore */ } storeP = null; }
}

/** Test-only: point at a fresh DB path without reusing the cached open. */
export function _resetKbStoreForTest(): void { storeP = null; }
Loading
Loading