@zerwiz/ymir 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/huginn-researcher.md +1 -1
- package/.agents/agents/kvasir-scout.md +1 -1
- package/.agents/agents/sindri-developer.md +1 -1
- package/.agents/backend/README.md +74 -0
- package/.agents/backend/model-bridge.py +74 -4
- package/.agents/config/cron.yaml +2 -0
- package/.agents/config/eindri-dispatch.json +6 -6
- package/.agents/migrations/0003-private-data-separation.sh +11 -1
- package/.agents/migrations/0005-roots-out-of-tree.sh +158 -0
- package/.agents/skills/README.md +1 -1
- package/.agents/skills/galdr-ymirsystem/SKILL.md +2 -1
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +73 -10
- package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +44 -1
- package/.agents/skills/galdr-ymirsystem/assets/installation.md +215 -23
- package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +3 -1
- package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +39 -1
- package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +4 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +7 -2
- package/.agents/skills/galdr-ymirsystem/assets/registry.md +1 -1
- package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +2 -2
- package/.agents/skills/galdr-ymirsystem/assets/smidja.md +16 -11
- package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +147 -4
- package/.agents/skills/groa-update/SKILL.md +20 -0
- package/.agents/skills/hamr-adapters/SKILL.md +2 -2
- package/.agents/skills/lifecycle/smoke_test.sh +7 -2
- package/.agents/skills/ymir-host/SKILL.md +2 -1
- package/.agents/skills/ymir-host/assets/install.md +69 -0
- package/AGENTS.md +78 -34
- package/CHANGELOG.md +1033 -1
- package/README.md +1 -1
- package/RULES/02-agents.md +2 -2
- package/RULES/04-hoard.md +32 -0
- package/RULES/06-append-only.md +50 -0
- package/RULES/07-config.md +56 -0
- package/RULES/09-electron.md +58 -0
- package/RULES/README.md +2 -0
- package/bin/a2a-mcp.sh +42 -7
- package/bin/agents-config.sh +30 -7
- package/bin/apodex-smoke-test.sh +14 -1
- package/bin/bifrost-bridge.sh +25 -6
- package/bin/bootstrap-macos.sh +6 -1
- package/bin/brokk-send.sh +14 -1
- package/bin/changelog-assemble.sh +154 -0
- package/bin/changelog-guard.sh +39 -7
- package/bin/crash-sense.sh +14 -1
- package/bin/design-check.sh +80 -0
- package/bin/design-icon.sh +153 -0
- package/bin/docs-guard.sh +2 -2
- package/bin/editor-place.sh +33 -15
- package/bin/eindri-acclaim.sh +14 -1
- package/bin/eindri-seat.sh +83 -0
- package/bin/eindri-seen.sh +14 -1
- package/bin/eindri-start.sh +14 -1
- package/bin/eir-doctor.sh +47 -1
- package/bin/feature-inventory.sh +73 -0
- package/bin/fleet-apply.sh +15 -2
- package/bin/github-deploy.sh +14 -1
- package/bin/gjallarhorn-expose.sh +43 -9
- package/bin/gjallarhorn-notify.sh +14 -1
- package/bin/gjallarhorn-purge.sh +14 -1
- package/bin/gjallarhorn-tunnel.sh +16 -3
- package/bin/groa-update.sh +9 -1
- package/bin/hall-snapshot.sh +21 -2
- package/bin/herdr-run.sh +23 -2
- package/bin/hlidskjalf-agents.sh +172 -0
- package/bin/hlidskjalf-usage.sh +196 -0
- package/bin/hoard-guard.sh +135 -0
- package/bin/hoard-lib.sh +91 -11
- package/bin/hodd.sh +28 -4
- package/bin/huginn-research-worker.sh +14 -1
- package/bin/local-model-lock.sh +22 -3
- package/bin/mimir-bridge.py +33 -0
- package/bin/mimir-bridge.sh +14 -1
- package/bin/mimir-reflect.sh +27 -0
- package/bin/mjollnir-webhook.sh +14 -1
- package/bin/mjollnir.sh +22 -3
- package/bin/model-hardware.sh +17 -3
- package/bin/model-resolve.sh +14 -1
- package/bin/nornir-job-hall-snapshot.sh +28 -0
- package/bin/nornir-job-nsr-compliance.sh +54 -0
- package/bin/npm-publish.sh +136 -0
- package/bin/omarchy-install.sh +18 -5
- package/bin/omarchy-plugins.sh +15 -2
- package/bin/omarchy-sense.sh +14 -1
- package/bin/pi-model-agnostic.sh +71 -0
- package/bin/private-guard.sh +20 -0
- package/bin/project-git.sh +18 -4
- package/bin/public-guard.sh +3 -2
- package/bin/sessrumnir-ensure.sh +1 -1
- package/bin/sessrumnir.sh +16 -3
- package/bin/smidja-bootstrap.sh +5 -1
- package/bin/syn-asset-pretool-check.sh +14 -1
- package/bin/syn-spawn-pretool-check.sh +58 -0
- package/bin/tailscale-sync.sh +15 -2
- package/bin/telegram-bot.sh +14 -1
- package/bin/ui-truth-check.sh +75 -0
- package/bin/valknut-load.sh +42 -4
- package/bin/wedge-notify.sh +14 -1
- package/bin/workspace-rag.sh +14 -1
- package/bin/ymir-install.sh +219 -54
- package/bin/ymir-plan.sh +356 -0
- package/bin/ymir-say.sh +14 -1
- package/bin/ymir-setup-auth.sh +12 -1
- package/bin/ymir-validate.sh +28 -8
- package/docs/lore.md +5 -1
- package/docs/runbooks/agents.md +1 -1
- package/package.json +13 -3
- package/scripts/electron.sh +74 -15
- package/scripts/lower.sh +34 -0
- package/scripts/raise.sh +42 -0
- package/scripts/start.sh +52 -2
- package/.agents/config/agents.yaml +0 -50
- package/.agents/config/tailscale-sync.yaml +0 -27
- package/.agents/memory/kaia.engram +0 -0
- package/.agents/memory/kaia.engram-shm +0 -0
- package/.agents/memory/kaia.engram-wal +0 -0
- package/.agents/memory/well/episodes.jsonl +0 -367
- package/.agents/memory/well/workspace.jsonl +0 -27
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# hlidskjalf-usage.sh — what the HARNESSES spent, not just the smithy.
|
|
3
|
+
#
|
|
4
|
+
# The Statistics gate counted only smithy runs (smidja.db), so a machine burning
|
|
5
|
+
# tokens through opencode and pi from morning to night reported "No runs yet". The
|
|
6
|
+
# sessions are the usage; this reads them.
|
|
7
|
+
#
|
|
8
|
+
# bin/hlidskjalf-usage.sh [--days N] # JSON, aggregated
|
|
9
|
+
#
|
|
10
|
+
# SOURCES (verified on this machine):
|
|
11
|
+
# opencode ~/.local/share/opencode/opencode.db (SQLite, Drizzle)
|
|
12
|
+
# session / message / part; usage lives in message.data as JSON:
|
|
13
|
+
# {role, modelID, providerID, cost, tokens:{total,input,output,
|
|
14
|
+
# reasoning,cache:{read,write}}}. cost is stored 0 - tokens are truth.
|
|
15
|
+
# pi ~/.pi/agent/sessions/**/*.jsonl
|
|
16
|
+
#
|
|
17
|
+
# BOUNDED BY CONSTRUCTION: the db here is 37 GB and holds 132k messages. Nothing
|
|
18
|
+
# loads rows into the UI - the aggregation is one SQL statement with json_extract,
|
|
19
|
+
# a time window, and a hard row cap, and the result is small by definition.
|
|
20
|
+
set -u
|
|
21
|
+
|
|
22
|
+
VERSION="1.0.0"
|
|
23
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
+
|
|
25
|
+
case "${1-}" in
|
|
26
|
+
-v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;;
|
|
27
|
+
-h|--help) sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
28
|
+
esac
|
|
29
|
+
|
|
30
|
+
DAYS="${YMIR_USAGE_DAYS:-30}"
|
|
31
|
+
while [ $# -gt 0 ]; do
|
|
32
|
+
case "$1" in --days) DAYS="${2:-30}"; shift 2 ;; *) shift ;; esac
|
|
33
|
+
done
|
|
34
|
+
|
|
35
|
+
DAYS="$DAYS" python3 - <<'PY'
|
|
36
|
+
import json, os, sqlite3, sys, glob
|
|
37
|
+
from datetime import datetime, timedelta, timezone
|
|
38
|
+
|
|
39
|
+
days = int(os.environ.get("DAYS", "30"))
|
|
40
|
+
since_ms = int((datetime.now(timezone.utc) - timedelta(days=days)).timestamp() * 1000)
|
|
41
|
+
out = {"window_days": days, "sources": {}, "totals": {}, "by_model": []}
|
|
42
|
+
|
|
43
|
+
oc = os.path.expanduser("~/.local/share/opencode/opencode.db")
|
|
44
|
+
if os.path.exists(oc):
|
|
45
|
+
try:
|
|
46
|
+
c = sqlite3.connect("file:%s?mode=ro" % oc, uri=True)
|
|
47
|
+
c.execute("PRAGMA query_only=ON")
|
|
48
|
+
# One statement, aggregated in the engine: never the rows, only the sums.
|
|
49
|
+
rows = c.execute("""
|
|
50
|
+
select coalesce(json_extract(data,'$.modelID'),'unknown') model,
|
|
51
|
+
count(*) msgs,
|
|
52
|
+
sum(coalesce(json_extract(data,'$.tokens.input'),0)) input,
|
|
53
|
+
sum(coalesce(json_extract(data,'$.tokens.output'),0)) output,
|
|
54
|
+
sum(coalesce(json_extract(data,'$.tokens.cache.read'),0)) cache_read,
|
|
55
|
+
sum(coalesce(json_extract(data,'$.tokens.cache.write'),0)) cache_write,
|
|
56
|
+
sum(coalesce(json_extract(data,'$.tokens.reasoning'),0)) reasoning
|
|
57
|
+
from message
|
|
58
|
+
where json_extract(data,'$.tokens') is not null
|
|
59
|
+
and coalesce(json_extract(data,'$.time.created'), 0) >= ?
|
|
60
|
+
group by model order by (sum(coalesce(json_extract(data,'$.tokens.input'),0))
|
|
61
|
+
+ sum(coalesce(json_extract(data,'$.tokens.output'),0))) desc
|
|
62
|
+
limit 40
|
|
63
|
+
""", (since_ms,)).fetchall()
|
|
64
|
+
rows = [r for r in rows if r[1]]
|
|
65
|
+
out["sources"]["opencode"] = {
|
|
66
|
+
"model": "harness",
|
|
67
|
+
"messages": sum(r[1] for r in rows),
|
|
68
|
+
"input": sum(r[2] for r in rows), "output": sum(r[3] for r in rows),
|
|
69
|
+
"cache_read": sum(r[4] for r in rows), "cache_write": sum(r[5] for r in rows),
|
|
70
|
+
"reasoning": sum(r[6] for r in rows),
|
|
71
|
+
}
|
|
72
|
+
out["by_model"] += [
|
|
73
|
+
{"source": "opencode", "model": r[0], "messages": r[1], "input": r[2],
|
|
74
|
+
"output": r[3], "cache_read": r[4], "cache_write": r[5], "reasoning": r[6]}
|
|
75
|
+
for r in rows
|
|
76
|
+
]
|
|
77
|
+
except Exception as e:
|
|
78
|
+
out["sources"]["opencode"] = {"error": str(e)[:120]}
|
|
79
|
+
|
|
80
|
+
# pi keeps TWO stores: the agent home and the older tree beside it. Scanning only
|
|
81
|
+
# one under-reported the harness by an order of magnitude (75 messages against 657
|
|
82
|
+
# session files), which is how a "small number" masks a missing source.
|
|
83
|
+
pi_files = []
|
|
84
|
+
for root in ("~/.pi/agent/sessions", "~/.pi/sessions"):
|
|
85
|
+
pi_files += glob.glob(os.path.expanduser(root + "/**/*.jsonl"), recursive=True)
|
|
86
|
+
if pi_files:
|
|
87
|
+
agg = {"input": 0, "output": 0, "cache_read": 0, "cache_write": 0, "messages": 0}
|
|
88
|
+
models = {}
|
|
89
|
+
for f in pi_files:
|
|
90
|
+
try:
|
|
91
|
+
with open(f, "r", errors="ignore") as fh:
|
|
92
|
+
for line in fh:
|
|
93
|
+
if '"tokens"' not in line and '"usage"' not in line:
|
|
94
|
+
continue
|
|
95
|
+
try:
|
|
96
|
+
d = json.loads(line)
|
|
97
|
+
except Exception:
|
|
98
|
+
continue
|
|
99
|
+
t = d.get("tokens") or d.get("usage") or {}
|
|
100
|
+
if not t:
|
|
101
|
+
continue
|
|
102
|
+
inp = t.get("input") or t.get("input_tokens") or 0
|
|
103
|
+
outp = t.get("output") or t.get("output_tokens") or 0
|
|
104
|
+
cr = (t.get("cache") or {}).get("read") or t.get("cache_read_input_tokens") or 0
|
|
105
|
+
cw = (t.get("cache") or {}).get("write") or t.get("cache_creation_input_tokens") or 0
|
|
106
|
+
agg["input"] += inp; agg["output"] += outp
|
|
107
|
+
agg["cache_read"] += cr; agg["cache_write"] += cw; agg["messages"] += 1
|
|
108
|
+
m = d.get("modelID") or d.get("model") or "unknown"
|
|
109
|
+
mm = models.setdefault(m, {"input": 0, "output": 0, "messages": 0})
|
|
110
|
+
mm["input"] += inp; mm["output"] += outp; mm["messages"] += 1
|
|
111
|
+
except Exception:
|
|
112
|
+
continue
|
|
113
|
+
out["sources"]["pi"] = {"model": "harness", **agg}
|
|
114
|
+
out["by_model"] += [{"source": "pi", "model": k, **v} for k, v in
|
|
115
|
+
sorted(models.items(), key=lambda kv: -(kv[1]["input"] + kv[1]["output"]))[:20]]
|
|
116
|
+
|
|
117
|
+
tot = {"messages": 0, "input": 0, "output": 0, "cache_read": 0, "cache_write": 0, "reasoning": 0}
|
|
118
|
+
for s in out["sources"].values():
|
|
119
|
+
for k in tot:
|
|
120
|
+
tot[k] += int(s.get(k) or 0)
|
|
121
|
+
tot["total"] = tot["input"] + tot["output"]
|
|
122
|
+
prompt = tot["input"] + tot["cache_read"]
|
|
123
|
+
# local = our own hardware; online = a cloud API. By model id, honestly.
|
|
124
|
+
LOCAL_MARKERS = ("llama", "lmstudio", "ollama", "local", "qwen3.6-35b-a3b")
|
|
125
|
+
for m in out["by_model"]:
|
|
126
|
+
mid = str(m.get("model", "")).lower()
|
|
127
|
+
m["kind"] = "local" if any(k in mid for k in LOCAL_MARKERS) else "online"
|
|
128
|
+
|
|
129
|
+
def _empty():
|
|
130
|
+
return {"tokens": 0, "calls": 0, "input": 0, "output": 0, "cache_read": 0}
|
|
131
|
+
lo = {"local": _empty(), "online": _empty()}
|
|
132
|
+
for m in out["by_model"]:
|
|
133
|
+
k = m.get("kind", "online")
|
|
134
|
+
lo[k]["tokens"] += int(m.get("input") or 0) + int(m.get("output") or 0)
|
|
135
|
+
lo[k]["calls"] += int(m.get("messages") or 0)
|
|
136
|
+
lo[k]["input"] += int(m.get("input") or 0)
|
|
137
|
+
lo[k]["output"] += int(m.get("output") or 0)
|
|
138
|
+
lo[k]["cache_read"] += int(m.get("cache_read") or 0)
|
|
139
|
+
out["local_online"] = lo
|
|
140
|
+
|
|
141
|
+
# the gate's section names, filled from the harnesses
|
|
142
|
+
oc = out["sources"].get("opencode", {})
|
|
143
|
+
pi = out["sources"].get("pi", {})
|
|
144
|
+
out["runs"] = {
|
|
145
|
+
"total": int(tot["messages"]), "success": int(tot["messages"]), "fail": 0, "running": 0,
|
|
146
|
+
"success_rate": 100.0 if tot["messages"] else 0.0,
|
|
147
|
+
"tokens": int(tot["input"]) + int(tot["output"]),
|
|
148
|
+
"cost": 0,
|
|
149
|
+
"by_source": {"opencode": int(oc.get("messages") or 0), "pi": int(pi.get("messages") or 0)},
|
|
150
|
+
}
|
|
151
|
+
out["by_chain"] = [
|
|
152
|
+
{"chain": m.get("source", "harness"), "workflow": m.get("source", "harness"),
|
|
153
|
+
"runs": int(m.get("messages") or 0), "success": int(m.get("messages") or 0),
|
|
154
|
+
"rate": 100.0, "tokens": int(m.get("input") or 0) + int(m.get("output") or 0), "cost": 0}
|
|
155
|
+
for m in out["by_model"][:12]
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
tot["cache_hit_ratio"] = round(tot["cache_read"] / prompt, 4) if prompt else 0.0
|
|
159
|
+
out["totals"] = tot
|
|
160
|
+
|
|
161
|
+
# THE GATE'S OWN NAMES, filled from the harnesses - so Statistics reports pi and
|
|
162
|
+
# opencode, and the smithy's runs stay the smithy's.
|
|
163
|
+
out["gate"] = {
|
|
164
|
+
"totals": {
|
|
165
|
+
"runs": int(tot["messages"]), "success": int(tot["messages"]), "fail": 0,
|
|
166
|
+
"running": 0, "tokens": int(tot["input"]) + int(tot["output"]), "cost": 0,
|
|
167
|
+
},
|
|
168
|
+
"usage": {
|
|
169
|
+
"input": int(tot["input"]), "output": int(tot["output"]),
|
|
170
|
+
"cache_read": int(tot["cache_read"]), "cache_write": int(tot["cache_write"]),
|
|
171
|
+
"total": int(tot["input"]) + int(tot["output"]),
|
|
172
|
+
},
|
|
173
|
+
"providers": {
|
|
174
|
+
"local": {"events": lo["local"]["calls"], "sessions": lo["local"]["calls"],
|
|
175
|
+
"tokens": lo["local"]["tokens"], "cost": 0,
|
|
176
|
+
"input": lo["local"]["tokens"], "output": 0, "cache_read": 0},
|
|
177
|
+
"online": {"events": lo["online"]["calls"], "sessions": lo["online"]["calls"],
|
|
178
|
+
"tokens": lo["online"]["tokens"], "cost": 0,
|
|
179
|
+
"input": lo["online"]["input"], "output": lo["online"]["output"], "cache_read": lo["online"]["cache_read"]},
|
|
180
|
+
"per_model": [
|
|
181
|
+
{"model": m["model"], "agent": m.get("source", "harness"), "kind": m.get("kind", "online"),
|
|
182
|
+
"cells": int(m.get("messages") or 0), "tokens": int(m.get("input") or 0) + int(m.get("output") or 0), "cost": 0}
|
|
183
|
+
for m in out["by_model"]
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
"by_chain": out["by_chain"],
|
|
187
|
+
"by_model": [
|
|
188
|
+
{"model": m["model"], "agent": m.get("source", "harness"), "kind": m.get("kind", "online"),
|
|
189
|
+
"cells": int(m.get("messages") or 0),
|
|
190
|
+
"tokens": int(m.get("input") or 0) + int(m.get("output") or 0), "cost": 0}
|
|
191
|
+
for m in out["by_model"]
|
|
192
|
+
],
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
print(json.dumps(out))
|
|
196
|
+
PY
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# hoard-guard.sh — the ward for the PRIVATE HOME repo ($YMIR_HOME).
|
|
3
|
+
#
|
|
4
|
+
# Why this exists: the public repo has six guards, the home had none — yet the
|
|
5
|
+
# home is where every private byte lives. The one real leak of 2026-09-17
|
|
6
|
+
# (platform.env.prev-fill, 44 credentials) happened HERE, in a scratch backup
|
|
7
|
+
# swept up by `git add -A`. Nothing was watching.
|
|
8
|
+
#
|
|
9
|
+
# This is not a script an agent must remember to run. It is seated as
|
|
10
|
+
# .git/hooks/pre-commit in the home repo, so GIT runs it on every commit —
|
|
11
|
+
# including the ones made in a hurry, by a loop, or by an agent that has never
|
|
12
|
+
# heard of it.
|
|
13
|
+
#
|
|
14
|
+
# bin/hoard-guard.sh # scan the staged change (used as pre-commit)
|
|
15
|
+
# bin/hoard-guard.sh --install # seat it in $YMIR_HOME/.git/hooks/pre-commit
|
|
16
|
+
# bin/hoard-guard.sh --all # scan every tracked file in the home
|
|
17
|
+
# bin/hoard-guard.sh --log-bypass <reason> # record a --no-verify commit
|
|
18
|
+
#
|
|
19
|
+
# Exit 1 on any hit, so the commit is blocked before the secret is committed.
|
|
20
|
+
set -u
|
|
21
|
+
|
|
22
|
+
VERSION="1.0.0"
|
|
23
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
+
# shellcheck source=bin/hoard-lib.sh
|
|
25
|
+
. "$SCRIPT_DIR/hoard-lib.sh"
|
|
26
|
+
hoard_root HOARD
|
|
27
|
+
HOME_REPO="$(cd "$HOARD/.." && pwd)"
|
|
28
|
+
LEDGER="$HOARD/memory/runes_audit.md"
|
|
29
|
+
|
|
30
|
+
case "${1:-}" in
|
|
31
|
+
-v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;;
|
|
32
|
+
-h|--help) sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
33
|
+
--install)
|
|
34
|
+
[ -d "$HOME_REPO/.git" ] || { printf 'hoard-guard: not a git repo: %s\n' "$HOME_REPO" >&2; exit 1; }
|
|
35
|
+
hook="$HOME_REPO/.git/hooks/pre-commit"
|
|
36
|
+
mkdir -p "$(dirname "$hook")" || exit 1
|
|
37
|
+
printf '#!/usr/bin/env bash\n# seated by bin/hoard-guard.sh --install — the ward for the private home\nexec "%s/hoard-guard.sh" "$@"\n' "$SCRIPT_DIR" >"$hook"
|
|
38
|
+
chmod +x "$hook"
|
|
39
|
+
printf 'hoard-guard[1]{action,path}:\n "install","%s"\n' "$hook"
|
|
40
|
+
exit 0 ;;
|
|
41
|
+
--log-bypass)
|
|
42
|
+
# Called when a --no-verify commit is detected, so a bypass is visible after
|
|
43
|
+
# the fact rather than silent.
|
|
44
|
+
mkdir -p "$(dirname "$LEDGER")" 2>/dev/null || true
|
|
45
|
+
printf '%s | hoard-guard BYPASS | %s | %s\n' \
|
|
46
|
+
"$(date -Is)" "${1:-no reason given}" "$(git -C "$HOME_REPO" log -1 --format=%h 2>/dev/null || echo unknown)" >>"$LEDGER" 2>/dev/null || true
|
|
47
|
+
printf 'hoard-guard[1]{action,logged}:\n "bypass","%s"\n' "$LEDGER"
|
|
48
|
+
exit 0 ;;
|
|
49
|
+
esac
|
|
50
|
+
|
|
51
|
+
# ── The checks ────────────────────────────────────────────────────────────────
|
|
52
|
+
# Every shape that caused, or could cause, a real leak.
|
|
53
|
+
SECRET_SHAPES='(AKIA[0-9A-Z]{16}|-----BEGIN [A-Z ]*PRIVATE KEY-----|-----BEGIN OPENSSH PRIVATE KEY-----|(ghp|gho|ghs|ghr)_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]+|AIza[0-9A-Za-z_-]{35}|npm_[A-Za-z0-9]{30,})'
|
|
54
|
+
|
|
55
|
+
# Filenames that must never be tracked: plaintext vault files, scratch backups,
|
|
56
|
+
# anything that is a copy of a secret under another name.
|
|
57
|
+
BAD_NAME_RE='(^|/)(platform\.env|id_rsa|id_ed25519|\.env\.local|\.env\.realm)$'
|
|
58
|
+
SCRATCH_RE='(\.prev-|\.prev\.|\.bak$|\.orig$|\.scratch$|\.save$|~$|\.env\.[0-9])'
|
|
59
|
+
# Deliberately allowed: age.key IS the vault key and is committed by decision
|
|
60
|
+
# (see hodd/docs/secrets-vault.md). It is exempt BY NAME, in one place, so the
|
|
61
|
+
# exception is auditable rather than accidental.
|
|
62
|
+
ALLOWED_RE='^hodd/secrets/age\.key$'
|
|
63
|
+
|
|
64
|
+
why() { printf 'hoard-guard: %s: %s\n' "$2" "$1" >&2; }
|
|
65
|
+
|
|
66
|
+
# A base64 blob whose DECODE contains a PEM header — the evasion that would have
|
|
67
|
+
# hidden a key. The data arrives on fd 3, NOT stdin: a heredoc on stdin would
|
|
68
|
+
# otherwise consume the pipe and the checker would read nothing.
|
|
69
|
+
check_base64_pem() { # <path-in-repo>; data on fd 3
|
|
70
|
+
local f=$1
|
|
71
|
+
python3 - "$f" /dev/fd/3 3<&3 <<'PY'
|
|
72
|
+
import base64, re, sys
|
|
73
|
+
try:
|
|
74
|
+
data = open(sys.argv[2], 'rb').read()
|
|
75
|
+
except OSError:
|
|
76
|
+
sys.exit(0)
|
|
77
|
+
for m in re.finditer(rb'[A-Za-z0-9+/=]{120,}', data):
|
|
78
|
+
chunk = m.group(0)
|
|
79
|
+
for cand in (chunk, chunk + b'=' * (-len(chunk) % 4)):
|
|
80
|
+
try:
|
|
81
|
+
dec = base64.b64decode(cand, validate=True)
|
|
82
|
+
except Exception:
|
|
83
|
+
continue
|
|
84
|
+
if b'PRIVATE KEY' in dec or b'BEGIN OPENSSH' in dec:
|
|
85
|
+
print(f"hoard-guard: possible secret in {sys.argv[1]} (base64-encoded key)", file=sys.stderr)
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
sys.exit(0)
|
|
88
|
+
PY
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
scan_list() {
|
|
92
|
+
local hit=0 f blob
|
|
93
|
+
while IFS= read -r f; do
|
|
94
|
+
[ -n "$f" ] || continue
|
|
95
|
+
|
|
96
|
+
case "$f" in
|
|
97
|
+
*/.gitignore|.gitignore) continue ;;
|
|
98
|
+
esac
|
|
99
|
+
if printf '%s' "$f" | grep -Eq "$ALLOWED_RE"; then continue; fi
|
|
100
|
+
|
|
101
|
+
if printf '%s' "$f" | grep -Eq "$BAD_NAME_RE"; then
|
|
102
|
+
why "$f" "plaintext secret file"; hit=1; continue
|
|
103
|
+
fi
|
|
104
|
+
if printf '%s' "$f" | grep -Eq "$SCRATCH_RE"; then
|
|
105
|
+
why "$f" "scratch/backup file — this is the shape that leaked on 2026-09-17"
|
|
106
|
+
hit=1; continue
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# Text only — a binary (sqlite, engram, image) has no secret shape to read
|
|
110
|
+
# and would only emit null-byte noise.
|
|
111
|
+
case "$f" in
|
|
112
|
+
*.engram|*.engram-wal|*.engram-shm|*.db|*.db-wal|*.db-shm|*.png|*.jpg|*.jpeg|*.gif|*.pdf|*.zip|*.gz|*.tar|*.woff|*.woff2|*.ttf|*.ico|*.icns) continue ;;
|
|
113
|
+
esac
|
|
114
|
+
blob="$(git -C "$HOME_REPO" show ":$f" 2>/dev/null | head -c 400000 | tr -d '\000')" || continue
|
|
115
|
+
[ -n "$blob" ] || continue
|
|
116
|
+
|
|
117
|
+
if printf '%s' "$blob" | grep -Eq "$SECRET_SHAPES"; then
|
|
118
|
+
why "$f" "possible secret"; hit=1; continue
|
|
119
|
+
fi
|
|
120
|
+
if ! check_base64_pem "$f" 3< <(printf '%s' "$blob"); then hit=1; fi
|
|
121
|
+
done
|
|
122
|
+
return "$hit"
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if [ "${1:-}" = "--all" ]; then
|
|
126
|
+
scan_list < <(git -C "$HOME_REPO" ls-files)
|
|
127
|
+
else
|
|
128
|
+
scan_list < <(git -C "$HOME_REPO" diff --cached --name-only --diff-filter=ACM)
|
|
129
|
+
fi || {
|
|
130
|
+
printf '\nhoard-guard: BLOCKED — the home repo is the vault. Do not commit this.\n' >&2
|
|
131
|
+
printf ' move the secret to hodd/secrets/platform.env.age (encrypted)\n' >&2
|
|
132
|
+
printf ' or delete the scratch file; see hodd/docs/secrets-vault.md\n' >&2
|
|
133
|
+
exit 1
|
|
134
|
+
}
|
|
135
|
+
exit 0
|
package/bin/hoard-lib.sh
CHANGED
|
@@ -1,20 +1,64 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# hoard-lib.sh — where
|
|
2
|
+
# hoard-lib.sh — where the operator's HOME lives, and the two roots beneath it.
|
|
3
3
|
#
|
|
4
|
-
# The
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# default).
|
|
4
|
+
# The law (RULES/04-hoard.md): no private thing — not a name, a key, a plan, a
|
|
5
|
+
# schedule, a client, a note — ever sits inside this repo. It lives in ONE place
|
|
6
|
+
# the operator OWNS and CHOSE at installation, and every script resolves that
|
|
7
|
+
# place HERE, so the answer can never drift between them (Rule 07: configuration
|
|
8
|
+
# is never hardcoded; ONE documented default).
|
|
10
9
|
#
|
|
11
|
-
#
|
|
10
|
+
# ymir_home_root the chosen home ($YMIR_HOME → the recorded choice → the default)
|
|
11
|
+
# hoard_root the hoard within it ($YMIR_HOARD → <home>/hodd)
|
|
12
|
+
# hoard_data_dir this machine's records ($YMIR_DATA_DIR → <hoard>/data)
|
|
13
|
+
# hoard_state_dir runtime state ($YMIR_STATE_DIR → <home>/state)
|
|
14
|
+
#
|
|
15
|
+
# The recorded choice is machine state, not user data: a path under
|
|
16
|
+
# `~/.config/ymir/` (the same place `engram-python` and `accounts.json` live), so
|
|
17
|
+
# a packaged install can be told where the home is without the home having to sit
|
|
18
|
+
# in the package. The tree is code; the home is the operator's.
|
|
19
|
+
#
|
|
20
|
+
# Source-safe, defines functions only; call `<fn> <result-var>`.
|
|
12
21
|
set -u
|
|
13
22
|
|
|
14
|
-
|
|
15
|
-
|
|
23
|
+
# Where a machine records its choices (never in the repo, never in the home).
|
|
24
|
+
YMIR_CONFIG_DIR="${YMIR_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/ymir}"
|
|
25
|
+
YMIR_HOME_DEFAULT="${YMIR_HOME_DEFAULT:-$HOME/Documents/Ymir}"
|
|
26
|
+
|
|
27
|
+
ymir_home_record() { # <result-var> — the home recorded at installation, or empty
|
|
28
|
+
local result_var=${1-} f="$YMIR_CONFIG_DIR/home"
|
|
16
29
|
[ -n "$result_var" ] || return 2
|
|
17
|
-
|
|
30
|
+
if [ -r "$f" ]; then
|
|
31
|
+
printf -v "$result_var" '%s' "$(head -n1 "$f" 2>/dev/null | tr -d '[:space:]')"
|
|
32
|
+
else
|
|
33
|
+
printf -v "$result_var" '%s' ""
|
|
34
|
+
fi
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ymir_home_root() { # <result-var> — the operator's home: env → recorded → default
|
|
38
|
+
local result_var=${1-} rec=""
|
|
39
|
+
[ -n "$result_var" ] || return 2
|
|
40
|
+
if [ -n "${YMIR_HOME:-}" ]; then
|
|
41
|
+
printf -v "$result_var" '%s' "$YMIR_HOME"; return 0
|
|
42
|
+
fi
|
|
43
|
+
ymir_home_record rec
|
|
44
|
+
printf -v "$result_var" '%s' "${rec:-$YMIR_HOME_DEFAULT}"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
ymir_home_record_set() { # <path> — record the operator's choice (machine state)
|
|
48
|
+
local home=${1-}
|
|
49
|
+
[ -n "$home" ] || return 2
|
|
50
|
+
mkdir -p "$YMIR_CONFIG_DIR" 2>/dev/null || return 1
|
|
51
|
+
printf '%s\n' "$home" >"$YMIR_CONFIG_DIR/home" || return 1
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
hoard_root() { # <result-var> — the hoard inside the home (may not exist yet)
|
|
55
|
+
local result_var=${1-} home
|
|
56
|
+
[ -n "$result_var" ] || return 2
|
|
57
|
+
if [ -n "${YMIR_HOARD:-}" ]; then
|
|
58
|
+
printf -v "$result_var" '%s' "$YMIR_HOARD"; return 0
|
|
59
|
+
fi
|
|
60
|
+
ymir_home_root home
|
|
61
|
+
printf -v "$result_var" '%s' "$home/hodd"
|
|
18
62
|
}
|
|
19
63
|
|
|
20
64
|
hoard_env() { # <result-var> — the platform env file inside the hoard
|
|
@@ -23,3 +67,39 @@ hoard_env() { # <result-var> — the platform env file inside the hoard
|
|
|
23
67
|
hoard_root root
|
|
24
68
|
printf -v "$result_var" '%s' "$root/secrets/platform.env"
|
|
25
69
|
}
|
|
70
|
+
|
|
71
|
+
# This machine's own records (operator, fleet, the host profile) and the runtime
|
|
72
|
+
# state (pids, logs, locks). Both live in the HOME — never in the code tree: a
|
|
73
|
+
# packaged install treats its tree as read-only and the next upgrade replaces it,
|
|
74
|
+
# so state written there is state lost. One documented default each; an env
|
|
75
|
+
# override wins, so a single machine can point a root elsewhere without a fork.
|
|
76
|
+
hoard_data_dir() { # <result-var> — where this machine's records live
|
|
77
|
+
local result_var=${1-} root
|
|
78
|
+
[ -n "$result_var" ] || return 2
|
|
79
|
+
hoard_root root
|
|
80
|
+
printf -v "$result_var" '%s' "${YMIR_DATA_DIR:-$root/data}"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
hoard_state_dir() { # <result-var> — runtime state: ephemeral, outside the tree
|
|
84
|
+
local result_var=${1-} home
|
|
85
|
+
[ -n "$result_var" ] || return 2
|
|
86
|
+
ymir_home_root home
|
|
87
|
+
printf -v "$result_var" '%s' "${YMIR_STATE_DIR:-$home/state}"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# The operator's SETTINGS — the agent set, cron, the local env file with its
|
|
91
|
+
# credentials. Settings are the operator's, not the package's: a packaged tree is
|
|
92
|
+
# replaced on upgrade, and a credential must never sit in a tree that ships.
|
|
93
|
+
hoard_settings_dir() { # <result-var> — the settings dir inside the home
|
|
94
|
+
local result_var=${1-} home
|
|
95
|
+
[ -n "$result_var" ] || return 2
|
|
96
|
+
ymir_home_root home
|
|
97
|
+
printf -v "$result_var" '%s' "${YMIR_SETTINGS_DIR:-$home/config}"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
hoard_local_env() { # <result-var> — the operator's env file (secrets, ports)
|
|
101
|
+
local result_var=${1-} home
|
|
102
|
+
[ -n "$result_var" ] || return 2
|
|
103
|
+
ymir_home_root home
|
|
104
|
+
printf -v "$result_var" '%s' "${YMIR_ENV_FILE:-$home/.env.local}"
|
|
105
|
+
}
|
package/bin/hodd.sh
CHANGED
|
@@ -42,6 +42,24 @@ PY
|
|
|
42
42
|
printf 'hodd[1]{action,file}:\n "load","%s"\n' "$f"
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
# Resolve a secrets path: if the plaintext is absent but an .age sibling and the
|
|
46
|
+
# hoard key exist, decrypt to stdout. Prints a path to a readable file (a temp
|
|
47
|
+
# when decrypted) via the result var. Secrets stay encrypted at rest; the agent
|
|
48
|
+
# never reads ciphertext directly — it asks for the resolved path.
|
|
49
|
+
resolve_secret() { # <file> <result-var>
|
|
50
|
+
local f=$1 var=$2 tmp key
|
|
51
|
+
if [ -r "$f" ]; then printf -v "$var" '%s' "$f"; return 0; fi
|
|
52
|
+
if [ -r "$f.age" ]; then
|
|
53
|
+
key="$HOARD/secrets/age.key"
|
|
54
|
+
command -v age >/dev/null 2>&1 || { printf 'error: %s is encrypted but age is not installed\nhelp: sudo pacman -S age\n' "$f.age" >&2; return 1; }
|
|
55
|
+
[ -r "$key" ] || { printf 'error: encrypted %s but no key at %s\n' "$f.age" "$key" >&2; return 1; }
|
|
56
|
+
tmp="$(mktemp)"; chmod 600 "$tmp"
|
|
57
|
+
age -d -i "$key" "$f.age" >"$tmp" 2>/dev/null || { rm -f "$tmp"; printf 'error: cannot decrypt %s\n' "$f.age" >&2; return 1; }
|
|
58
|
+
printf -v "$var" '%s' "$tmp"; return 0
|
|
59
|
+
fi
|
|
60
|
+
printf 'error: not readable: %s (and no %s.age)\n' "$f" "$f" >&2; return 1
|
|
61
|
+
}
|
|
62
|
+
|
|
45
63
|
case "$ACTION" in
|
|
46
64
|
path) printf '%s\n' "$HOARD" ;;
|
|
47
65
|
init)
|
|
@@ -56,23 +74,29 @@ case "$ACTION" in
|
|
|
56
74
|
load)
|
|
57
75
|
f="${1:-}"; [ -n "$f" ] || { printf 'error: load needs a path under the Hoard\n' >&2; exit 2; }
|
|
58
76
|
case "$f" in /*) ;; *) f="$HOARD/$f" ;; esac
|
|
59
|
-
|
|
77
|
+
resolve_secret "$f" rf || exit 1
|
|
78
|
+
load_env "$rf"
|
|
79
|
+
case "$rf" in "$f") ;; *) rm -f "$rf" ;; esac ;;
|
|
60
80
|
emit)
|
|
61
81
|
# Print `export KEY=value` lines (quoting-safe) for the CALLER to eval, so
|
|
62
82
|
# `eval "$(bin/hodd.sh emit <file>)"` sets the vars in the invoking shell.
|
|
83
|
+
# Transparently decrypts an .age sibling when the plaintext is absent.
|
|
63
84
|
f="${1:-}"; [ -n "$f" ] || { printf 'error: emit needs a path under the Hoard\n' >&2; exit 2; }
|
|
64
85
|
case "$f" in /*) ;; *) f="$HOARD/$f" ;; esac
|
|
65
|
-
|
|
66
|
-
python3 - "$
|
|
86
|
+
resolve_secret "$f" rf || exit 1
|
|
87
|
+
python3 - "$rf" <<'PY'
|
|
67
88
|
import re, shlex, sys
|
|
68
89
|
for line in open(sys.argv[1]):
|
|
69
90
|
m = re.match(r'^([A-Za-z_][A-Za-z0-9_]*)=(.*)$', line.rstrip("\n"))
|
|
70
91
|
if m:
|
|
71
92
|
print(f"export {m.group(1)}={shlex.quote(m.group(2))}")
|
|
72
93
|
PY
|
|
94
|
+
case "$rf" in "$f") ;; *) rm -f "$rf" ;; esac
|
|
73
95
|
;;
|
|
74
96
|
tenant)
|
|
75
97
|
t="${1:-}"; [ -n "$t" ] || { printf 'error: tenant needs a name\n' >&2; exit 2; }
|
|
76
|
-
|
|
98
|
+
resolve_secret "$HOARD/tenants/$t/.env" rf || exit 1
|
|
99
|
+
load_env "$rf"
|
|
100
|
+
case "$rf" in "$HOARD/tenants/$t/.env") ;; *) rm -f "$rf" ;; esac ;;
|
|
77
101
|
*) printf 'error: unknown action %s\nhelp: bin/hodd.sh [path|init|ls|load|emit|tenant]\n' "$ACTION" >&2; exit 2 ;;
|
|
78
102
|
esac
|
|
@@ -12,6 +12,19 @@ set -u
|
|
|
12
12
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
13
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
14
14
|
|
|
15
|
+
# The operator's settings and secrets live in the home they chose, never in the
|
|
16
|
+
# code tree — a packaged install replaces its tree on upgrade, and a credential
|
|
17
|
+
# must never sit in a tree that ships (Rule 04).
|
|
18
|
+
if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
|
|
19
|
+
_yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
20
|
+
for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
|
|
21
|
+
[ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
|
|
22
|
+
done
|
|
23
|
+
unset _yr _yc
|
|
24
|
+
fi
|
|
25
|
+
hoard_settings_dir YMIR_SETTINGS_DIR
|
|
26
|
+
hoard_local_env YMIR_ENV_FILE
|
|
27
|
+
|
|
15
28
|
BRIEF="" OUTDIR="" MODEL=""
|
|
16
29
|
while [ $# -gt 0 ]; do
|
|
17
30
|
case "$1" in
|
|
@@ -28,7 +41,7 @@ done
|
|
|
28
41
|
mkdir -p "$OUTDIR"
|
|
29
42
|
|
|
30
43
|
# Resolve env — source .env.local if present.
|
|
31
|
-
for f in "$
|
|
44
|
+
for f in "$YMIR_ENV_FILE" "${YMIR_HOARD:-$HOME/Documents/Ymir}/.env.local"; do
|
|
32
45
|
[ -f "$f" ] && . "$f" 2>/dev/null || true
|
|
33
46
|
done
|
|
34
47
|
|
package/bin/local-model-lock.sh
CHANGED
|
@@ -12,8 +12,27 @@ set -u
|
|
|
12
12
|
|
|
13
13
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
14
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
|
|
16
|
+
# The operator's settings and secrets live in the home they chose, never in the
|
|
17
|
+
# code tree — a packaged install replaces its tree on upgrade, and a credential
|
|
18
|
+
# must never sit in a tree that ships (Rule 04).
|
|
19
|
+
if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
|
|
20
|
+
_yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
+
for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
|
|
22
|
+
[ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
|
|
23
|
+
done
|
|
24
|
+
unset _yr _yc
|
|
25
|
+
fi
|
|
26
|
+
hoard_settings_dir YMIR_SETTINGS_DIR
|
|
27
|
+
hoard_local_env YMIR_ENV_FILE
|
|
28
|
+
hoard_state_dir YMIR_STATE_DIR
|
|
29
|
+
hoard_data_dir YMIR_DATA_DIR
|
|
30
|
+
|
|
31
|
+
# The roots that live OUTSIDE the code tree: this machine's records and the
|
|
32
|
+
# runtime state belong to the home the operator chose at installation, never in
|
|
33
|
+
# the tree — a packaged install replaces its tree on upgrade (Rule 04).
|
|
34
|
+
CFG="${YMIR_AGENTS_YAML:-$YMIR_SETTINGS_DIR/agents.yaml}"
|
|
35
|
+
LOCK="$YMIR_STATE_DIR/local-model.lock"
|
|
17
36
|
WAIT="${LOCAL_MODEL_WAIT:-1800}"
|
|
18
37
|
|
|
19
38
|
[ $# -gt 0 ] || { printf 'error: usage: bin/local-model-lock.sh <command...>\n' >&2; exit 2; }
|
|
@@ -31,7 +50,7 @@ PY
|
|
|
31
50
|
)"
|
|
32
51
|
[ -n "$N" ] || N=1
|
|
33
52
|
|
|
34
|
-
mkdir -p "$
|
|
53
|
+
mkdir -p "$YMIR_STATE_DIR"
|
|
35
54
|
|
|
36
55
|
if [ "$N" -le 1 ]; then
|
|
37
56
|
# one at a time — exclusive lock, wait for the peer to finish
|
package/bin/mimir-bridge.py
CHANGED
|
@@ -49,6 +49,35 @@ def engine(agent: str | None) -> Engram:
|
|
|
49
49
|
return _pool[key]
|
|
50
50
|
|
|
51
51
|
|
|
52
|
+
def layers():
|
|
53
|
+
"""What the well actually holds, layer by layer - the record a UI reads."""
|
|
54
|
+
out = {"facts": 0, "facts_active": 0, "facts_superseded": 0, "entities": 0,
|
|
55
|
+
"edges": 0, "reflections": 0, "last_reflection": None, "vec_index": 0}
|
|
56
|
+
try:
|
|
57
|
+
con = sqlite3.connect(f"file:{STORE}?mode=ro", uri=True)
|
|
58
|
+
for key, sql in (
|
|
59
|
+
("facts", "select count(*) from facts"),
|
|
60
|
+
("facts_active", "select count(*) from facts where superseded_at is null"),
|
|
61
|
+
("facts_superseded", "select count(*) from facts where superseded_at is not null"),
|
|
62
|
+
("entities", "select count(*) from entities"),
|
|
63
|
+
("edges", "select count(*) from edges"),
|
|
64
|
+
("reflections", "select count(*) from reflections"),
|
|
65
|
+
("vec_index", "select count(*) from vec_episodes_rowids"),
|
|
66
|
+
):
|
|
67
|
+
try:
|
|
68
|
+
out[key] = int(con.execute(sql).fetchone()[0])
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
try:
|
|
72
|
+
out["last_reflection"] = con.execute(
|
|
73
|
+
"select max(finished_at) from reflections").fetchone()[0]
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
except Exception as e:
|
|
77
|
+
out["error"] = str(e)
|
|
78
|
+
return out
|
|
79
|
+
|
|
80
|
+
|
|
52
81
|
def count_episodes() -> int:
|
|
53
82
|
try:
|
|
54
83
|
con = sqlite3.connect(f"file:{STORE}?mode=ro", uri=True)
|
|
@@ -98,10 +127,14 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
98
127
|
"agents": engine(None).list_agents(),
|
|
99
128
|
})
|
|
100
129
|
if u.path == "/inspect":
|
|
130
|
+
# THE LAYERS, not just the count. The smithy shows what it is told,
|
|
131
|
+
# and it was told three fields while the well holds six: a panel that
|
|
132
|
+
# reads only episodes cannot show that facts and entities exist.
|
|
101
133
|
return self._json(200, {
|
|
102
134
|
"store": str(STORE),
|
|
103
135
|
"episodes": count_episodes(),
|
|
104
136
|
"agents": engine(None).list_agents(),
|
|
137
|
+
**layers(),
|
|
105
138
|
})
|
|
106
139
|
if u.path == "/recent":
|
|
107
140
|
limit = int((q.get("limit") or ["60"])[0])
|
package/bin/mimir-bridge.sh
CHANGED
|
@@ -18,6 +18,19 @@ fi
|
|
|
18
18
|
VERSION="1.0.0"
|
|
19
19
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
20
20
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
21
|
+
|
|
22
|
+
# The roots that live OUTSIDE the code tree: this machine's records and the
|
|
23
|
+
# runtime state belong to the home the operator chose at installation, never in
|
|
24
|
+
# the tree — a packaged install replaces its tree on upgrade (Rule 04).
|
|
25
|
+
if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
|
|
26
|
+
_yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
27
|
+
for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
|
|
28
|
+
[ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
|
|
29
|
+
done
|
|
30
|
+
unset _yr _yc
|
|
31
|
+
fi
|
|
32
|
+
hoard_state_dir YMIR_STATE_DIR
|
|
33
|
+
hoard_data_dir YMIR_DATA_DIR
|
|
21
34
|
YMIR_HOME="${YMIR_HOME:-$HOME/Documents/Ymir}"
|
|
22
35
|
BRIDGE="$ROOT/bin/mimir-bridge.py"
|
|
23
36
|
DB="${ENGRAM_DB:-$YMIR_HOME/memory/kaia.engram}"
|
|
@@ -75,7 +88,7 @@ if ! "$PY" -c "import engram" 2>/dev/null; then
|
|
|
75
88
|
printf 'error: engram not installed for %s\nhelp: bin/prereq-ensure.sh engram (installs it into a compatible Python)\n' "$PY" >&2; exit 1
|
|
76
89
|
fi
|
|
77
90
|
|
|
78
|
-
mkdir -p "$
|
|
91
|
+
mkdir -p "$YMIR_STATE_DIR" "$(dirname "$DB")"
|
|
79
92
|
ENGRAM_DB="$DB" MIMIRSBRUNN_PORT="$PORT" nohup "$PY" "$BRIDGE" >"$LOG_FILE" 2>&1 &
|
|
80
93
|
echo $! >"$PID_FILE"
|
|
81
94
|
sleep 3
|