Phase 13: integrations (research folder, git, Prometheus, signed webhooks)

Adds the integration layer that turns external sources into provenance-tagged
observations without ever executing what it reads.

- research folder poller: os.OpenRoot, text allowlist, binary/archive/symlink
  rejection, content fingerprints, duplicate tracking
- git poller: bare shallow mirror with hooks disabled and ext protocol refused,
  commit observations, co-change pairs
- Prometheus poller: threshold crossings and relative-change detection
- webhooks: HMAC over timestamp.body, +/-300s window, replay cache, 256 KiB cap,
  per-IP rate limit; refused entirely without a secret
- capabilities read_metrics (allow) and repository.read (ask), scoped to the
  invoking mind's integrations
- migrations 0013, API, UI pages, noemactl integrations, docs/integrations.md
This commit is contained in:
Andy Dixon
2026-06-29 21:04:50 +01:00
parent b35dbe0299
commit 89edbb7c06
26 changed files with 2391 additions and 7 deletions
+3
View File
@@ -0,0 +1,3 @@
DROP TABLE IF EXISTS ingested_documents;
DROP TABLE IF EXISTS integration_runs;
DROP TABLE IF EXISTS integrations;
+53
View File
@@ -0,0 +1,53 @@
-- Integrations: configured external sources, their runs and ingested documents.
CREATE TABLE integrations (
id uuid PRIMARY KEY,
mind_id uuid NOT NULL REFERENCES minds(id),
kind text NOT NULL CHECK (kind IN ('research_folder','git','prometheus','webhook')),
name text NOT NULL,
config jsonb NOT NULL DEFAULT '{}'::jsonb, -- never contains secrets
secret_ref text NOT NULL DEFAULT '', -- env:/file:/enc: reference
trust text NOT NULL DEFAULT 'source',
enabled boolean NOT NULL DEFAULT true,
status text NOT NULL DEFAULT 'new' CHECK (status IN ('new','ok','stale','error','disabled')),
state jsonb NOT NULL DEFAULT '{}'::jsonb, -- poller bookkeeping (last commit, last values)
last_run_at timestamptz,
last_ok_at timestamptz,
last_error text NOT NULL DEFAULT '',
ingested_count integer NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX integrations_mind_name_idx ON integrations (mind_id, lower(name));
CREATE TABLE integration_runs (
id uuid PRIMARY KEY,
integration_id uuid NOT NULL REFERENCES integrations(id) ON DELETE CASCADE,
started_at timestamptz NOT NULL DEFAULT now(),
finished_at timestamptz,
items integer NOT NULL DEFAULT 0,
observations integer NOT NULL DEFAULT 0,
errors integer NOT NULL DEFAULT 0,
notes jsonb NOT NULL DEFAULT '[]'::jsonb,
error text NOT NULL DEFAULT ''
);
CREATE INDEX integration_runs_idx ON integration_runs (integration_id, started_at DESC);
CREATE TABLE ingested_documents (
id uuid PRIMARY KEY,
mind_id uuid NOT NULL REFERENCES minds(id),
integration_id uuid NOT NULL REFERENCES integrations(id) ON DELETE CASCADE,
fingerprint text NOT NULL,
reference text NOT NULL,
title text NOT NULL DEFAULT '',
content_type text NOT NULL DEFAULT '',
size_bytes bigint NOT NULL DEFAULT 0,
extracted_chars integer NOT NULL DEFAULT 0,
excerpt text NOT NULL DEFAULT '',
status text NOT NULL CHECK (status IN ('ingested','duplicate','rejected','failed')),
error text NOT NULL DEFAULT '',
event_id uuid,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (mind_id, fingerprint)
);
CREATE INDEX ingested_documents_integration_idx ON ingested_documents (integration_id, created_at DESC);