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:
@@ -224,3 +224,6 @@ canvas.graph { width: 100%; height: 560px; display: block; border-radius: 8px; b
|
||||
.tablewrap { overflow-x: auto; }
|
||||
.approval { border: 1px solid var(--border); border-radius: 8px; padding: .8rem 1rem; margin-bottom: .8rem; }
|
||||
.approval-head { margin-bottom: .5rem; }
|
||||
|
||||
/* Integrations */
|
||||
.badge.is-ok { background: var(--ok-soft); color: var(--ok); } .badge.is-stale { background: var(--warn-soft); color: var(--warn); } .badge.is-error { background: var(--danger-soft); color: var(--danger); } .badge.is-new, .badge.is-disabled { background: var(--surface-2); color: var(--muted); }
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{{define "content"}}
|
||||
{{$i := .Data.Integration}}
|
||||
<div class="pagehead mindhead"><div><p class="muted small"><a href="/integrations">Integrations</a> › {{$i.Kind}}</p><h1>{{$i.Name}} <span class="badge is-{{$i.Status}}">{{$i.Status}}</span></h1><p class="muted">Mind {{.Data.MindName}} · trust {{$i.Trust}} · every {{$i.Interval}}{{if $i.SecretSet}} · secret set{{end}}{{with $i.LastError}} · <span class="danger">{{.}}</span>{{end}}</p></div>
|
||||
<div class="row">{{if and (.Principal.User.Role.AtLeast "operator") (ne $i.Kind "webhook")}}<form method="post" action="/integrations/{{$i.ID}}/run"><input type="hidden" name="_csrf" value="{{.CSRF}}"><button class="btn">Run now</button></form>{{end}}
|
||||
{{if .Principal.User.Role.AtLeast "administrator"}}<form method="post" action="/integrations/{{$i.ID}}/toggle"><input type="hidden" name="_csrf" value="{{.CSRF}}"><button class="btn">{{if $i.Enabled}}Disable{{else}}Enable{{end}}</button></form>
|
||||
<form method="post" action="/integrations/{{$i.ID}}/delete"><input type="hidden" name="_csrf" value="{{.CSRF}}"><button class="btn btn-danger" data-confirm="Delete this integration? Ingested observations remain in history.">Delete</button></form>{{end}}</div></div>
|
||||
<section class="grid cols-2">
|
||||
<div class="card"><h2>Configuration</h2><pre class="json">{{pretty (jsonOf $i.Config)}}</pre>
|
||||
{{if eq $i.Kind "webhook"}}<h3>Delivery</h3><p class="small">POST to <code>/api/v1/webhooks/{{$i.ID}}</code> with headers <code>X-Noema-Timestamp</code> (unix seconds) and <code>X-Noema-Signature: sha256=HMAC_SHA256(secret, timestamp + "." + body)</code>. Body: <code>{"title":"…","text":"…","reference":"…","salience":0.6}</code>. Unsigned deliveries and replays are refused.</p>{{end}}
|
||||
{{if $i.State}}<h3>State</h3><pre class="json">{{pretty (jsonOf $i.State)}}</pre>{{end}}</div>
|
||||
<div class="card"><h2>Recent runs</h2>{{if .Data.Runs}}<table class="table compact"><thead><tr><th>Started</th><th class="num">Items</th><th class="num">Observations</th><th class="num">Errors</th><th>Notes</th></tr></thead><tbody>{{range .Data.Runs}}<tr><td class="small muted nowrap">{{.StartedAt.Local.Format "01-02 15:04:05"}}</td><td class="num">{{.Items}}</td><td class="num">{{.Observations}}</td><td class="num {{if .Errors}}danger{{end}}">{{.Errors}}</td><td class="small">{{with .Error}}<span class="danger">{{.}}</span> {{end}}<span class="muted">{{trunc (printf "%s" .Notes) 160}}</span></td></tr>{{end}}</tbody></table>{{else}}<p class="muted">No runs yet.</p>{{end}}</div>
|
||||
</section>
|
||||
{{if .Data.Documents}}<div class="card"><h2>Ingested documents</h2><table class="table compact"><thead><tr><th>Status</th><th>Title</th><th>Type</th><th class="num">Bytes</th><th class="num">Chars</th><th>Reference</th><th></th></tr></thead><tbody>{{range .Data.Documents}}<tr><td><span class="badge {{if eq .Status "ingested"}}ok{{else if eq .Status "rejected"}}danger{{else}}{{end}}">{{.Status}}</span>{{with .Error}}<div class="small muted">{{.}}</div>{{end}}</td><td>{{.Title}}</td><td class="small">{{.ContentType}}</td><td class="num">{{.SizeBytes}}</td><td class="num">{{.ExtractedChars}}</td><td class="small mono">{{trunc .Reference 60}}</td><td>{{if .EventID}}<a class="small" href="/events/{{.EventID}}">event</a>{{end}}</td></tr>{{end}}</tbody></table></div>{{end}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,30 @@
|
||||
{{define "content"}}
|
||||
<div class="pagehead"><h1>Integrations</h1><p class="muted">External sources feeding observations into minds. Everything ingested is data: tagged with source, trust and reference, never obeyed.</p></div>
|
||||
{{$admin := .Principal.User.Role.AtLeast "administrator"}}
|
||||
<section class="grid cols-3-1">
|
||||
<div class="card">
|
||||
{{if .Data.Integrations}}<table class="table compact"><thead><tr><th>Name</th><th>Kind</th><th>Mind</th><th>Trust</th><th>Status</th><th>Last run</th><th class="num">Ingested</th><th></th></tr></thead>
|
||||
<tbody>{{range .Data.Integrations}}<tr><td><a href="/integrations/{{.ID}}"><strong>{{.Name}}</strong></a></td><td class="small">{{.Kind}}</td><td class="small">{{index $.Data.MindNames .MindID}}</td><td><span class="badge">{{.Trust}}</span></td>
|
||||
<td><span class="badge is-{{.Status}}">{{.Status}}</span>{{with .LastError}} <span class="small danger" title="{{.}}">{{trunc . 40}}</span>{{end}}</td><td class="small muted">{{if .LastRunAt}}{{ago .LastRunAt}}{{else}}never{{end}}</td><td class="num">{{.IngestedCount}}</td>
|
||||
<td>{{if and ($.Principal.User.Role.AtLeast "operator") (ne .Kind "webhook")}}<form method="post" action="/integrations/{{.ID}}/run" class="inline"><input type="hidden" name="_csrf" value="{{$.CSRF}}"><button class="btn small">Run now</button></form>{{end}}</td></tr>{{end}}</tbody></table>
|
||||
{{else}}<p class="muted">No integrations yet.</p>{{end}}
|
||||
</div>
|
||||
{{if $admin}}
|
||||
<div class="card">
|
||||
<h2>Add integration</h2>
|
||||
<form method="post" action="/integrations" class="stack">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||
<label>Mind <select name="mind">{{range .Data.Minds}}<option value="{{.Slug}}">{{.DisplayName}}</option>{{end}}</select></label>
|
||||
<label>Kind <select name="kind"><option value="research_folder">research folder (watched directory)</option><option value="git">git repository</option><option value="prometheus">Prometheus / VictoriaMetrics</option><option value="webhook">signed webhook</option></select></label>
|
||||
<label>Name <input name="name" required maxlength="64"></label>
|
||||
<label>Path / URL / endpoint <input name="target" placeholder="./research · https://host/repo.git · http://prom:9090"></label>
|
||||
<label>Interval (seconds) <input type="number" name="interval_seconds" value="300" min="30"></label>
|
||||
<label>Metric queries <span class="muted small">(prometheus: one per line, name = expr [gt|lt threshold])</span> <textarea name="queries" rows="3" class="mono" placeholder="cpu = 100 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m]))*100 gt 80"></textarea></label>
|
||||
<label>Secret <input type="password" name="secret" autocomplete="off" placeholder="webhook HMAC secret or bearer token; env:NAME works"></label>
|
||||
<label class="check"><input type="checkbox" name="allow_private"> Endpoint may be a private address</label>
|
||||
<div><button class="btn btn-primary" type="submit">Add</button></div>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user