instar 0.28.82 → 0.28.84
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/dashboard/index.html +239 -0
- package/dist/cli.js +10 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/memoryBackfillEvidence.d.ts +69 -0
- package/dist/commands/memoryBackfillEvidence.d.ts.map +1 -0
- package/dist/commands/memoryBackfillEvidence.js +220 -0
- package/dist/commands/memoryBackfillEvidence.js.map +1 -0
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +72 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/core/EvolutionManager.d.ts +73 -0
- package/dist/core/EvolutionManager.d.ts.map +1 -1
- package/dist/core/EvolutionManager.js +376 -0
- package/dist/core/EvolutionManager.js.map +1 -1
- package/dist/core/SharedStateLedger.d.ts.map +1 -1
- package/dist/core/SharedStateLedger.js +4 -0
- package/dist/core/SharedStateLedger.js.map +1 -1
- package/dist/core/types.d.ts +90 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +8 -0
- package/dist/core/types.js.map +1 -1
- package/dist/memory/EvidenceRenderer.d.ts +64 -0
- package/dist/memory/EvidenceRenderer.d.ts.map +1 -0
- package/dist/memory/EvidenceRenderer.js +119 -0
- package/dist/memory/EvidenceRenderer.js.map +1 -0
- package/dist/memory/SemanticMemory.d.ts +61 -1
- package/dist/memory/SemanticMemory.d.ts.map +1 -1
- package/dist/memory/SemanticMemory.js +381 -11
- package/dist/memory/SemanticMemory.js.map +1 -1
- package/dist/server/AgentServer.d.ts +4 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +2 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +8 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +493 -3
- package/dist/server/routes.js.map +1 -1
- package/dist/tasks/DivergenceChecker.d.ts +89 -0
- package/dist/tasks/DivergenceChecker.d.ts.map +1 -0
- package/dist/tasks/DivergenceChecker.js +255 -0
- package/dist/tasks/DivergenceChecker.js.map +1 -0
- package/dist/tasks/LruCache.d.ts +33 -0
- package/dist/tasks/LruCache.d.ts.map +1 -0
- package/dist/tasks/LruCache.js +88 -0
- package/dist/tasks/LruCache.js.map +1 -0
- package/dist/tasks/RateLimiter.d.ts +61 -0
- package/dist/tasks/RateLimiter.d.ts.map +1 -0
- package/dist/tasks/RateLimiter.js +108 -0
- package/dist/tasks/RateLimiter.js.map +1 -0
- package/dist/tasks/TaskFlowDueWaker.d.ts +29 -0
- package/dist/tasks/TaskFlowDueWaker.d.ts.map +1 -0
- package/dist/tasks/TaskFlowDueWaker.js +73 -0
- package/dist/tasks/TaskFlowDueWaker.js.map +1 -0
- package/dist/tasks/TaskFlowMaintenanceSweeper.d.ts +40 -0
- package/dist/tasks/TaskFlowMaintenanceSweeper.d.ts.map +1 -0
- package/dist/tasks/TaskFlowMaintenanceSweeper.js +131 -0
- package/dist/tasks/TaskFlowMaintenanceSweeper.js.map +1 -0
- package/dist/tasks/TaskFlowRegistry.d.ts +180 -0
- package/dist/tasks/TaskFlowRegistry.d.ts.map +1 -0
- package/dist/tasks/TaskFlowRegistry.js +791 -0
- package/dist/tasks/TaskFlowRegistry.js.map +1 -0
- package/dist/tasks/ThreadlineFlowBridge.d.ts +54 -0
- package/dist/tasks/ThreadlineFlowBridge.d.ts.map +1 -0
- package/dist/tasks/ThreadlineFlowBridge.js +129 -0
- package/dist/tasks/ThreadlineFlowBridge.js.map +1 -0
- package/dist/tasks/task-flow-registry.store.sqlite.d.ts +77 -0
- package/dist/tasks/task-flow-registry.store.sqlite.d.ts.map +1 -0
- package/dist/tasks/task-flow-registry.store.sqlite.js +350 -0
- package/dist/tasks/task-flow-registry.store.sqlite.js.map +1 -0
- package/dist/tasks/task-flow-types.d.ts +229 -0
- package/dist/tasks/task-flow-types.d.ts.map +1 -0
- package/dist/tasks/task-flow-types.js +128 -0
- package/dist/tasks/task-flow-types.js.map +1 -0
- package/dist/threadline/ThreadlineNicknames.d.ts +88 -0
- package/dist/threadline/ThreadlineNicknames.d.ts.map +1 -0
- package/dist/threadline/ThreadlineNicknames.js +195 -0
- package/dist/threadline/ThreadlineNicknames.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +78 -78
- package/upgrades/0.28.83.md +45 -0
- package/upgrades/0.28.84.md +89 -0
- package/upgrades/side-effects/taskflow-phase1.md +130 -0
- package/upgrades/side-effects/taskflow-phase2.md +110 -0
- package/upgrades/side-effects/taskflow-phase3a.md +138 -0
- package/upgrades/side-effects/taskflow-phase5.md +234 -0
- package/upgrades/side-effects/threadline-nickname-resolver-authority.md +81 -0
- package/upgrades/side-effects/wikiclaim-evidence-phase1.md +138 -0
- package/upgrades/side-effects/wikiclaim-evidence-phase4.md +129 -0
- package/upgrades/side-effects/wikiclaim-evidence-phase5.md +285 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Side-Effects Review — WikiClaim Evidence Phase 4 (HTTP + dashboard)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `wikiclaim-evidence-phase4`
|
|
4
|
+
**Date:** 2026-05-10
|
|
5
|
+
**Author:** Echo
|
|
6
|
+
**Second-pass reviewer:** required (auth-principal-to-viewer-scope mapping; XSS; visibility-filter correctness)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Adds inverse-traceability HTTP endpoints + dashboard panels that consume the typed evidence APIs landed in Phase 1 and hardened in Phase 5. No new policy, no new storage shape, no new filter — this PR is a thin pass-through from HTTP/dashboard to `SemanticMemory.getEvidence` / `SemanticMemory.findCitations` / `SemanticMemory.getEntityWithEvidence`.
|
|
11
|
+
|
|
12
|
+
Spec: `docs/specs/OPENCLAW-IMPORT-WIKICLAIM-EVIDENCE-SPEC.md` § Phase 4 (line 343), Inverse traceability (line 291), Storage and Privacy (line 310).
|
|
13
|
+
|
|
14
|
+
What lands:
|
|
15
|
+
- `GET /memory/evidence/by-entity/:id?viewerScope=...` — returns the entity's evidence array, viewer-scope filtered. Routes through `getEntityWithEvidence` so the entity-level visibility check and the per-row evidence-tier filter both fire BEFORE any bytes leave the storage layer. 404 collapses "entity missing" and "entity hidden at viewer scope" into the same response per spec § Storage and Privacy line 316 (non-leaky inverse-query rule extended to direct fetch).
|
|
16
|
+
- `GET /memory/entities/by-evidence?kind=...&sourceId=...&viewerScope=...` — returns entities citing `(kind, sourceId)`. Pass-through to `findCitations`, which filters by BOTH the citing entity's `privacyScope` AND the citing evidence row's `privacyTier`. Returns `{kind, sourceId, viewerScope, entities, totalResults}`.
|
|
17
|
+
- `viewerScope` resolution: optional query param, default `private` (full visibility for the agent's own DB; the single-bearer-token auth model has one principal). Narrowing-only — callers can request a more restrictive view to preview "what would a topic-peer see?" Invalid scope values fall back silently to the default rather than 400'ing, mirroring how other Express handlers in this file treat unknown enum input.
|
|
18
|
+
- Evidence kind whitelist on the inverse endpoint — `VALID_EVIDENCE_KINDS` set matches `MemoryEvidenceKind` exactly. Unknown kinds → 400 (this is mechanic-level validation, not judgment).
|
|
19
|
+
- Auth: both routes sit behind the global `authMiddleware` applied at app level in `AgentServer`. No route-local auth code (and no new code to maintain that could drift).
|
|
20
|
+
- Dashboard "Evidence" tab: per-entity evidence panel (text input + Lookup button → render evidence list with kind, sourceId, file:line, weight, confidence, privacyTier badge, note); "what cites this?" panel (kind dropdown + sourceId input → render entities with type, name, id, scope, confidence, and an "Inspect evidence" button that pivots to the per-entity panel). One viewer-scope dropdown drives both panels.
|
|
21
|
+
- Dashboard XSS posture: every user-supplied string (entity id, name, content, sourceId, note, path, privacyTier label) flows through the local `escapeHtml` helper before reaching `innerHTML`. The "Inspect evidence" button uses a `data-entity-id` attribute + a delegated `addEventListener('click', …)` — NOT an inline `onclick` string interpolation — so even pathologically-shaped IDs cannot break out of the attribute context.
|
|
22
|
+
|
|
23
|
+
Files touched:
|
|
24
|
+
- `src/server/routes.ts` — adds `/memory/evidence/by-entity/:id` and `/memory/entities/by-evidence` after the existing `/semantic/*` routes; adds local `VALID_EVIDENCE_KINDS`, `VALID_VIEWER_SCOPES`, `resolveViewerScope`.
|
|
25
|
+
- `dashboard/index.html` — adds an "Evidence" tab button to the tab bar, an `evidenceTab` panel container, a registry entry in `TAB_REGISTRY`, and a JS section (`evCurrentViewerScope`, `evRenderTierBadge`, `evRenderEvidenceRow`, `evRenderEntityRow`, `loadEvidenceByEntity`, `loadEntitiesByEvidence`, `evLookupFromCitation`) inserted before `loadTokens`.
|
|
26
|
+
- `tests/unit/memory-evidence-routes.test.ts` — 14 vitest cases against real SQLite + a real Express app with the production `authMiddleware`.
|
|
27
|
+
- `upgrades/side-effects/wikiclaim-evidence-phase4.md` (this file)
|
|
28
|
+
|
|
29
|
+
## Decision-point inventory
|
|
30
|
+
|
|
31
|
+
- `resolveViewerScope(raw)` — **add** — mechanic-level enum lookup against `VALID_VIEWER_SCOPES`. Not judgment.
|
|
32
|
+
- `VALID_EVIDENCE_KINDS` lookup on the inverse endpoint — **add** — mechanic-level enum match. Not judgment.
|
|
33
|
+
- `escapeHtml`-on-every-field render path — **add** — structural HTML escape, not judgment.
|
|
34
|
+
- 404-vs-200-empty collapse on `by-entity` — **add** — spec-mandated non-leak (§ Storage and Privacy line 316).
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 1. Over-block
|
|
39
|
+
|
|
40
|
+
**`viewerScope` query param ignored when invalid (default-to-private).** A caller sending `?viewerScope=garbage` does not get a 400; they get private-scope results. Rationale: viewerScope is narrowing-only and the agent's principal is already `private`; widening doesn't exist, so misspellings can only narrow OR be ignored. The safest fallback for a typo is the agent's own scope (private) — i.e., NO over-block. A stricter 400 would be a usability regression with no privacy benefit.
|
|
41
|
+
|
|
42
|
+
**`findCitations` returns an empty array on missing entities or zero matches.** Both paths look identical to the caller. By design — the inverse query is exactly the "is there any citation of this source anywhere I can see?" probe; differentiating "no matches" from "no visible matches at your scope" would itself be the leak the spec is closing.
|
|
43
|
+
|
|
44
|
+
## 2. Under-block
|
|
45
|
+
|
|
46
|
+
- **Rate limiting:** these are read endpoints with no LLM call, no mutation, no fan-out. Per the patterns in routes.ts, hot read endpoints in `/semantic/*` (e.g. `/semantic/recall/:id`, `/semantic/search`, `/semantic/explore/:id`) ALSO don't carry a `rateLimiter` wrapper — they rely on the global auth gate. The new routes follow the same convention. If abuse appears in observability, attaching `rateLimiter(60_000, 60)` is a one-line follow-up; the per-entity evidence cap (50 default, 500 hard) caps the response size regardless.
|
|
47
|
+
- **viewerScope authentication binding:** the `viewerScope` query param is caller-controlled. This is acceptable because the auth model has ONE principal (the agent itself, full bearer token = `private` ceiling), and the param can only *narrow*. A caller cannot widen to `private` if they were given a `shared-project` token, because there is no such token — the system has one. When multi-tenant auth lands (not in scope), this resolver becomes the seam: it must read principal claims off the request and clamp the param to the principal's ceiling. Currently documented in the inline comment on `resolveViewerScope`.
|
|
48
|
+
- **`note` field rendering on the dashboard:** notes are free-form text, capped at 500 bytes at write time (Phase 1). The dashboard renders them via `escapeHtml`, NOT `marked` / `DOMPurify` — even though those libraries are loaded for the file viewer. Treating notes as plain text is the safer default; if markdown rendering is wanted later, route through `DOMPurify.sanitize(marked.parse(note))` like the file viewer does.
|
|
49
|
+
- **Dashboard does not paginate** when the inverse query returns many entities. The per-entity evidence cap means the forward endpoint is bounded; the inverse endpoint is bounded only by "how many entities cite this source" which has no hard cap. v1 dashboards expect human-scale lookups; a follow-up can add `limit`/`offset` if needed.
|
|
50
|
+
|
|
51
|
+
## 3. Level-of-abstraction fit
|
|
52
|
+
|
|
53
|
+
Right layer.
|
|
54
|
+
|
|
55
|
+
- HTTP route is a 30-line pass-through to the storage primitive. Privacy decisions live in `SemanticMemory.getEvidence` / `findCitations` (read-time filter) and in `EvidenceRenderer` (the documented enforcement point for already-loaded entities, per Phase 5). The route does not re-decide visibility — it serializes whatever the storage layer returned.
|
|
56
|
+
- Dashboard panels invoke the route via `apiFetch` and render the result. They do not contain a copy of the privacy filter; they trust the server to have filtered.
|
|
57
|
+
- `viewerScope` derivation is owned by `resolveViewerScope` — one function, one place to retrofit when multi-tenant auth lands.
|
|
58
|
+
|
|
59
|
+
## 4. Signal vs authority compliance
|
|
60
|
+
|
|
61
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
62
|
+
|
|
63
|
+
- [x] **No** — these are read endpoints with no judgment-level block/allow surface.
|
|
64
|
+
|
|
65
|
+
The "decisions" in this PR:
|
|
66
|
+
- Mechanic-level enum lookups (`VALID_VIEWER_SCOPES`, `VALID_EVIDENCE_KINDS`);
|
|
67
|
+
- Mechanic-level scope ordering (already owned by `EvidenceRenderer`, not duplicated here);
|
|
68
|
+
- Mechanic-level escape on the dashboard.
|
|
69
|
+
|
|
70
|
+
None of those answers the question "should this evidence be visible to this caller?" — that question is answered exactly once, in `SemanticMemory`. The route is dumb; the renderer is the boundary.
|
|
71
|
+
|
|
72
|
+
## 5. Interactions
|
|
73
|
+
|
|
74
|
+
- **Phase 1 (#137):** consumes `getEntityWithEvidence`, `getEvidence`, `findCitations` — all already viewer-scope-filtered. No re-wiring of those methods.
|
|
75
|
+
- **Phase 5 (#141):** the `EvidenceRenderer` helper is the documented single enforcement point. This PR uses storage-layer methods that internally use the same predicates that `EvidenceRenderer` uses (`isEntityVisibleAtScope`, `isEvidenceVisibleAtScope`); the rules are colocated, no drift. If a future caller has an already-loaded entity in memory and wants to ship it to a viewer, they should call `renderEvidenceForScope` — these routes don't go through that path because they load fresh from SQLite.
|
|
76
|
+
- **Phase 2 (#139) and Phase 3 (#142) in flight:** they touch producer code (EvolutionManager, DispatchExecutor, DecisionJournal). They don't touch routes.ts or the dashboard. Conflict-free.
|
|
77
|
+
- **TaskFlow Phase 5 (#143 merged d7d4eba9):** added rate-limit middleware to `/flows/*`. This PR does NOT add rate-limit to `/memory/*` evidence routes — the convention in `/semantic/*` is no per-route limiter, relying on global auth + per-entity bound. If a follow-up wants to standardize, it's a one-liner.
|
|
78
|
+
- **Dashboard tab registry:** new `evidence` entry sits between `threadline` and `secrets`. The mobile nav menu / `updateNavToggleLabel` keys off `data-tab="evidence"` and works without changes (the existing logic reads the button text dynamically).
|
|
79
|
+
- **`authMiddleware`:** the existing global middleware sits in front of every non-public route. Both new routes inherit it without local code. Test file mounts `authMiddleware(AUTH_TOKEN)` directly to exercise the 401 path.
|
|
80
|
+
|
|
81
|
+
## 6. External surfaces
|
|
82
|
+
|
|
83
|
+
- **Other agents on the same machine:** none. The routes return only this agent's own data.
|
|
84
|
+
- **Other users of the install base:** two new GET routes appear. No breaking changes to existing routes. Anyone hitting `/memory/*` paths today gets a 404 (we use those URLs first time here, no collision check needed — `grep` confirms no `/memory/evidence/*` or `/memory/entities/*` exists in routes.ts before this PR).
|
|
85
|
+
- **External systems:** none.
|
|
86
|
+
- **Persistent state:** none — read-only routes.
|
|
87
|
+
- **Privacy posture:** unchanged. The visibility surface is exactly what `SemanticMemory` already exposes; this PR only adds an HTTP/dashboard frontend.
|
|
88
|
+
- **Cloudflare tunnel:** if the agent exposes its server via Cloudflare tunnel, the new routes are reachable from the internet under the same bearer-token auth as everything else. No new exposure shape; the auth boundary is unchanged.
|
|
89
|
+
|
|
90
|
+
## 7. Rollback cost
|
|
91
|
+
|
|
92
|
+
- **Hot-fix release:** pure additive change; `git revert <merge-commit>` ships as the next patch.
|
|
93
|
+
- **Data migration:** none.
|
|
94
|
+
- **Agent state repair:** none. No state written.
|
|
95
|
+
- **User visibility:** mild — the Evidence tab disappears from the dashboard. No data loss; the underlying DB is untouched.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Conclusion
|
|
100
|
+
|
|
101
|
+
Phase 4 ships two read-only HTTP routes plus a single dashboard tab. All privacy decisions remain in the Phase 1 storage-layer filter and the Phase 5 `EvidenceRenderer` boundary; this PR does not introduce a second filter. Auth is the existing bearer-token model. XSS is structurally prevented by `escapeHtml`-on-every-interpolation plus the absence of inline `onclick` string-interpolation patterns. 14 new vitest cases cover viewer-scope filtering, auth, 404, 400, empty results, and the inverse-leak cross-product. Cleared to ship pending second-pass concurrence on auth/XSS/filter-correctness.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Second-pass review
|
|
106
|
+
|
|
107
|
+
**Reviewer:** independent code-audit subagent (adversarial), one round.
|
|
108
|
+
**Round 1 findings:**
|
|
109
|
+
|
|
110
|
+
1. **Inline `onclick` with `${id}` interpolation in `evRenderEntityRow`** — flagged at draft time. `escapeHtml` does encode `'`/`"`, so a payload like `id="x'),alert(1);//"` would render as `'`, structurally inert. Still: an inline `onclick="..."` string-build is a brittle pattern; one future edit forgetting the escape is a foot-gun. **Resolution:** rewrote as `data-entity-id="${id}"` + `addEventListener` delegation. No string-interpolation into an HTML attribute that becomes a JS expression at parse time.
|
|
111
|
+
2. **`viewerScope` widening attempt** — a caller could send `?viewerScope=private` even if they "shouldn't" have private access. **Resolution:** the auth model has one principal (the agent itself = private ceiling). There is no "shouldn't" right now — the bearer token IS the private credential. The resolver is the documented seam for future multi-tenant scope clamping, and the inline comment in routes.ts says so. Cleared.
|
|
112
|
+
3. **Visibility-filter correctness on `by-entity` for a private entity at shared-project viewer** — confirmed: `getEntityWithEvidence` returns `null` when the entity scope exceeds the viewer scope (`SemanticMemory.ts:775`). Route returns 404 in that case. Test `returns 404 (non-leaky) when a private entity is requested at shared-project scope` proves it.
|
|
113
|
+
4. **Inverse leak via `by-evidence` when entity scope is shared-project but evidence row is private** — confirmed: `findCitations` checks BOTH entity scope AND evidence tier (`SemanticMemory.ts:752-754`). Test `cross-product: shared-project viewer cannot see private-tier evidence inverse query` covers this exact shape. Cleared.
|
|
114
|
+
5. **`viewerScope` enum bypass via prototype-pollution / non-string input** — `req.query.viewerScope` may be a string, an array, or undefined. `resolveViewerScope` only returns one of the three known scopes; anything else falls to `'private'`. Even with `?viewerScope=__proto__`, the `VALID_VIEWER_SCOPES.has()` check rejects (`Set.prototype.has` does not match own/prototype keys of arbitrary strings). Cleared.
|
|
115
|
+
|
|
116
|
+
Cleared to ship.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Evidence pointers
|
|
121
|
+
|
|
122
|
+
- New tests: `npx vitest run tests/unit/memory-evidence-routes.test.ts` → 14/14 passing (102ms).
|
|
123
|
+
- Typecheck: `npx tsc --noEmit` → clean.
|
|
124
|
+
- Spec source of truth: `docs/specs/OPENCLAW-IMPORT-WIKICLAIM-EVIDENCE-SPEC.md` (Phase 4 at line 343).
|
|
125
|
+
- Predecessor PRs: #137 (Phase 1 schema), #141 (Phase 5 backfill + render hardening).
|
|
126
|
+
- Privacy enforcement evidence:
|
|
127
|
+
- `SemanticMemory.getEntityWithEvidence` (entity scope filter): `src/memory/SemanticMemory.ts:765-779`
|
|
128
|
+
- `SemanticMemory.findCitations` (dual filter on entity scope + evidence tier): `src/memory/SemanticMemory.ts:735-759`
|
|
129
|
+
- `EvidenceRenderer` (documented enforcement boundary): `src/memory/EvidenceRenderer.ts`
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# Side-Effects Review — WikiClaim Evidence Phase 5 (backfill CLI + render hardening)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `wikiclaim-evidence-phase5`
|
|
4
|
+
**Date:** 2026-05-10
|
|
5
|
+
**Author:** Echo
|
|
6
|
+
**Second-pass reviewer:** required (idempotency + producer-narrowing-respected + cross-product privacy)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Ships the final WikiClaim phase per spec § Migration Plan line 347:
|
|
11
|
+
|
|
12
|
+
1. `instar memory backfill-evidence [--dry-run]` — one-shot, idempotent
|
|
13
|
+
CLI that walks every existing `MemoryEntity`, pattern-matches the legacy
|
|
14
|
+
`source: string` field, and synthesizes a single `MemoryEvidence` row
|
|
15
|
+
of `kind: 'external-url'` when (and only when) the source string is an
|
|
16
|
+
anchored `https?://…` URL. Any non-URL source string is left alone —
|
|
17
|
+
`evidence: []` per spec § Risks line 357 ("no LLM in the migration path").
|
|
18
|
+
2. `src/memory/EvidenceRenderer.ts` — single privacy-enforcement helper
|
|
19
|
+
exposing `renderEvidenceForScope(entity, viewerScope)`. Per spec
|
|
20
|
+
§ Storage and Privacy line 315 the renderer is THE enforcement boundary;
|
|
21
|
+
this lands the helper now (before Phase 4 HTTP/dashboard rendering
|
|
22
|
+
touches evidence in user-facing output) so every downstream consumer
|
|
23
|
+
has exactly one place to filter from. `SemanticMemory`'s own read paths
|
|
24
|
+
are switched to delegate to the helper for the entity-visible /
|
|
25
|
+
evidence-visible predicates — keeping one source of truth.
|
|
26
|
+
|
|
27
|
+
### Phase 5 scope deliberately narrowed vs spec § Migration line 208–212
|
|
28
|
+
|
|
29
|
+
The spec lists four legacy-source patterns:
|
|
30
|
+
|
|
31
|
+
| Pattern | Backfilled to | Phase 5 status |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `https?://…` | `kind: 'external-url'` | **shipped** |
|
|
34
|
+
| `session:ABC` | `kind: 'session'` | **skipped** |
|
|
35
|
+
| `user:Justin` | `kind: 'message'` | **skipped** |
|
|
36
|
+
| `observation` / any other | `evidence: []` | unchanged (no write) |
|
|
37
|
+
|
|
38
|
+
The narrowing is structural, not a deferral. Per spec § Producers line 229
|
|
39
|
+
and the Phase 1 `PRODUCER_KIND_ALLOWLIST`, the `manual` producer (the only
|
|
40
|
+
producer this CLI can invoke from outside any of the in-process subsystems)
|
|
41
|
+
is allowed to write `external-url` ONLY. The `session` and `message` kinds
|
|
42
|
+
require the `DecisionJournal` and `LearnSkill` producers respectively, which
|
|
43
|
+
have access to the originating subsystem's context (session metadata,
|
|
44
|
+
conversation transcript) that the CLI does not. **We deliberately do NOT
|
|
45
|
+
widen the allowlist to make migration easier** — that would unwind the
|
|
46
|
+
producer-kind capability boundary the spec installs.
|
|
47
|
+
|
|
48
|
+
A future bridge that walks legacy `session:` and `user:` sources from
|
|
49
|
+
inside DecisionJournal / LearnSkill (with full producer context) is a
|
|
50
|
+
clean follow-up and explicitly tracked in MEMORY.md.
|
|
51
|
+
|
|
52
|
+
What lands:
|
|
53
|
+
|
|
54
|
+
- `src/commands/memoryBackfillEvidence.ts` — pattern-match + dup-check +
|
|
55
|
+
`addEvidence('manual')` call.
|
|
56
|
+
- `src/cli.ts` — register `memory backfill-evidence` subcommand with
|
|
57
|
+
`--dry-run` flag and `-d <dir>`.
|
|
58
|
+
- `src/memory/EvidenceRenderer.ts` — `renderEvidenceForScope`,
|
|
59
|
+
`filterEvidenceArrayForScope`, `isEntityVisibleAtScope`,
|
|
60
|
+
`isEvidenceVisibleAtScope`. The latter two are imported by
|
|
61
|
+
`SemanticMemory.ts` so the read-path predicates are no longer locally
|
|
62
|
+
defined — they delegate to the renderer.
|
|
63
|
+
- `tests/unit/memory-backfill-evidence.test.ts` — 17 vitest cases:
|
|
64
|
+
URL pattern match, non-URL skip, producer narrowing respected,
|
|
65
|
+
idempotency across two runs, idempotency across entity privacy tiers,
|
|
66
|
+
dry-run is non-mutating, apply-after-dry-run still writes, privacy
|
|
67
|
+
inherits across every entity scope.
|
|
68
|
+
- `tests/unit/evidence-render-privacy.test.ts` — cross-product table:
|
|
69
|
+
3 entity scopes × 5 evidence tiers × 3 viewer scopes (45 combinations)
|
|
70
|
+
plus six edge-case assertions. Spec § Risks line 356 ("render every
|
|
71
|
+
privacyScope × privacyTier combination and assert no leak") satisfied.
|
|
72
|
+
- `upgrades/side-effects/wikiclaim-evidence-phase5.md` (this file).
|
|
73
|
+
|
|
74
|
+
## Decision-point inventory
|
|
75
|
+
|
|
76
|
+
- URL anchoring (`^https?:\/\/\S+$`) — **add** — hard-invariant pattern
|
|
77
|
+
match at the migration entry. Not judgment. Anchoring (no embedded URLs
|
|
78
|
+
inside other text) keeps the migration deterministic across re-runs as
|
|
79
|
+
the matcher evolves.
|
|
80
|
+
- Idempotency dup-check (`existing.some(ev.kind === 'external-url' &&
|
|
81
|
+
(ev.path === source || ev.sourceId === source))`) — **add** —
|
|
82
|
+
equality-based. Not judgment.
|
|
83
|
+
- Dup-check viewer-scope (`'private'` default, configurable) — **add** —
|
|
84
|
+
reads the widest scope so the dup-check sees every existing row. A
|
|
85
|
+
narrower default would silently allow a duplicate write against a
|
|
86
|
+
private-tier evidence row on a shared-project entity. Tests pin this.
|
|
87
|
+
- Producer narrowing (`addEvidence(..., 'manual')` only) — **enforced**
|
|
88
|
+
by `PRODUCER_KIND_ALLOWLIST`; the CLI inherits that gate, does not
|
|
89
|
+
re-implement it.
|
|
90
|
+
- `privacyTier: undefined` on synthesized rows — **add** — per spec
|
|
91
|
+
§ Risks line 360, never auto-upgrade.
|
|
92
|
+
- `confidence: 0.5` on synthesized rows — **add** — matches spec
|
|
93
|
+
§ Migration line 210's suggested low-confidence value for coarse
|
|
94
|
+
legacy provenance.
|
|
95
|
+
- `updatedAt: entity.createdAt` — **add** — preserves temporal ordering
|
|
96
|
+
and means a second run on the same entity produces identical content.
|
|
97
|
+
- `EvidenceRenderer` helper — **add** — single-enforcement render path;
|
|
98
|
+
no ad-hoc render exists today (Phase 4 HTTP/dashboard hasn't shipped)
|
|
99
|
+
so this is preventive. `SemanticMemory`'s existing read-path
|
|
100
|
+
predicates are switched to delegate to the helper so we have ONE
|
|
101
|
+
visibility-comparison map across the codebase.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 1. Over-block
|
|
106
|
+
|
|
107
|
+
**URL anchoring rejects embedded URLs:** `source: "see https://example.com"`
|
|
108
|
+
is skipped, not backfilled. By design — extracting a partial URL creates
|
|
109
|
+
ambiguity ("which one of the URLs in this string is the real source?")
|
|
110
|
+
and re-runs could backfill different URLs as the extractor evolves.
|
|
111
|
+
Producers that need to retroactively cite an embedded URL can run a
|
|
112
|
+
follow-up migration with a more permissive extractor; we keep the v1
|
|
113
|
+
backfill conservative.
|
|
114
|
+
|
|
115
|
+
**Whitespace not trimmed:** `source: " https://x.com "` is skipped.
|
|
116
|
+
Same reason — trim semantics depend on convention; v1 keeps the rule
|
|
117
|
+
"the source IS the URL, not a string containing a URL."
|
|
118
|
+
|
|
119
|
+
**Producer-narrowing rejects `session:` / `user:`:** Already covered
|
|
120
|
+
above. These need their own producer bridges, not this CLI. Explicitly
|
|
121
|
+
tracked.
|
|
122
|
+
|
|
123
|
+
## 2. Under-block
|
|
124
|
+
|
|
125
|
+
- **`external-url` is not URL-validated:** Beyond the `https?://\S+`
|
|
126
|
+
anchor we don't parse the URL. A malformed-but-anchored value like
|
|
127
|
+
`https://[invalid:` would be backfilled. The renderer treats
|
|
128
|
+
`external-url.path` as display-only per spec § Threat Model line 372
|
|
129
|
+
("renderers MUST treat `path` for `external-url` as display-only —
|
|
130
|
+
never auto-fetch"), so an invalid URL is a display-time annoyance,
|
|
131
|
+
not an SSRF or injection vector.
|
|
132
|
+
- **`note` cap not retroactively applied:** We set a fixed
|
|
133
|
+
`'Backfilled from legacy MemoryEntity.source (Phase 5 migration)'`
|
|
134
|
+
note (52 bytes, well under the 500-byte cap). No risk.
|
|
135
|
+
- **Concurrency:** The CLI doesn't take a DB lock. If a producer is
|
|
136
|
+
actively writing while backfill runs, both will succeed; the dup-check
|
|
137
|
+
loses the race for entities that get a fresh `external-url` evidence
|
|
138
|
+
row from a producer mid-scan. Mitigation: dup-check on every entity
|
|
139
|
+
before write (already present); on race, the producer's row lands
|
|
140
|
+
first and ours is skipped on second run. Operationally the CLI is
|
|
141
|
+
intended for offline / quiet-period invocation; this is documented.
|
|
142
|
+
|
|
143
|
+
## 3. Level-of-abstraction fit
|
|
144
|
+
|
|
145
|
+
Right layer. The backfill belongs at the CLI / command layer because:
|
|
146
|
+
- It uses public `SemanticMemory` APIs (`export()`, `getEvidence()`,
|
|
147
|
+
`addEvidence()`) — no private-state access.
|
|
148
|
+
- It's a one-shot operator-invoked migration, not a runtime path.
|
|
149
|
+
- Pattern matching is at the edge (the CLI), not inside storage.
|
|
150
|
+
|
|
151
|
+
`EvidenceRenderer` belongs in `src/memory/` because it owns evidence-shape
|
|
152
|
+
filtering logic; pulling it out of `SemanticMemory.ts` (without dragging
|
|
153
|
+
better-sqlite3 with it) lets non-SQLite render contexts (HTTP shapers,
|
|
154
|
+
dashboard JSON producers) use it without spurious dependencies.
|
|
155
|
+
|
|
156
|
+
## 4. Signal vs authority compliance
|
|
157
|
+
|
|
158
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
159
|
+
|
|
160
|
+
- [x] **No** — this change has no judgment-level block/allow surface.
|
|
161
|
+
|
|
162
|
+
The backfill's "skip" outcomes are mechanic-level pattern misses
|
|
163
|
+
(`URL_SOURCE_PATTERN.test(source) === false`) or idempotency dedupes
|
|
164
|
+
(equality-based dup-check). The renderer's filtering is mechanic-level
|
|
165
|
+
ordering comparison (`tier ordinal >= evidence tier ordinal`). No
|
|
166
|
+
intelligent gate, no judgment surface.
|
|
167
|
+
|
|
168
|
+
The actual semantic question — "should this entity be cited by an
|
|
169
|
+
external URL?" — was already answered by whoever set `source` on the
|
|
170
|
+
entity. The CLI replays that answer into typed evidence; it does not
|
|
171
|
+
re-decide.
|
|
172
|
+
|
|
173
|
+
## 5. Interactions
|
|
174
|
+
|
|
175
|
+
- **Phase 2 producers landing in parallel:** EvolutionManager /
|
|
176
|
+
DispatchExecutor producers write `feedback`, `pattern-entity`,
|
|
177
|
+
`job-run`, `ledger-entry`, `supersedes-evidence` rows. Backfill only
|
|
178
|
+
writes `external-url`. Zero overlap; dup-check is keyed on
|
|
179
|
+
`(kind === 'external-url' && path === source)` so even if Phase 2 had
|
|
180
|
+
attached an `external-url` row via a different producer (none of them
|
|
181
|
+
are allowed to per the allowlist), the URL-equality dup-check still
|
|
182
|
+
catches it.
|
|
183
|
+
- **Phase 3 DecisionJournal / LearnSkill:** Same story — different
|
|
184
|
+
kinds; non-overlapping dup-check keys.
|
|
185
|
+
- **`SemanticMemory.getEvidence` / `findCitations` / `getEntityWithEvidence`**:
|
|
186
|
+
Now delegate to the `EvidenceRenderer.isEntityVisibleAtScope` and
|
|
187
|
+
`isEvidenceVisibleAtScope` exports for the entity-visible /
|
|
188
|
+
evidence-visible predicates. The Phase 1 in-file copies are removed.
|
|
189
|
+
Verified by re-running the full Phase 1 test suite
|
|
190
|
+
(`semantic-memory-evidence.test.ts` 21/21, `semantic-memory.test.ts`
|
|
191
|
+
49/49, `semantic-memory-privacy.test.ts` 32/32) — all green.
|
|
192
|
+
- **JSONL append log:** Every `addEvidence` call in the backfill emits
|
|
193
|
+
the existing `addEvidence` JSONL action; replay path is unchanged
|
|
194
|
+
(Phase 1's deferred-handler note about JSONL replay still applies —
|
|
195
|
+
unaffected by Phase 5).
|
|
196
|
+
- **No HTTP / dashboard / threadline render today:** Phase 4 hasn't
|
|
197
|
+
landed; there is no ad-hoc evidence renderer in current production
|
|
198
|
+
code to replace. The helper is preventive — landing it now means
|
|
199
|
+
Phase 4 has exactly one place to call into when it adds HTTP routes
|
|
200
|
+
and dashboard panels. **No ad-hoc paths were found and replaced.**
|
|
201
|
+
This is documented as the intent rather than presented as completed
|
|
202
|
+
retro-replacement work.
|
|
203
|
+
|
|
204
|
+
## 6. External surfaces
|
|
205
|
+
|
|
206
|
+
- **Other agents on the same machine**: None. Backfill is per-agent,
|
|
207
|
+
per-state-directory.
|
|
208
|
+
- **Other users of the install base**: A new subcommand appears on
|
|
209
|
+
`instar memory --help`. Default behavior of `instar memory <other>`
|
|
210
|
+
is unchanged. Running the CLI is opt-in.
|
|
211
|
+
- **External systems**: None. Backfill is local-only; the
|
|
212
|
+
`external-url` rows it writes store the URL as a string for display,
|
|
213
|
+
never fetched (per spec § Threat Model line 372).
|
|
214
|
+
- **Persistent state**: One `addEvidence` JSONL row per backfilled
|
|
215
|
+
entity, one `entity_evidence` table row per backfilled entity.
|
|
216
|
+
Bounded by the number of entities with URL sources.
|
|
217
|
+
- **Privacy posture**: Tighter — backfilled rows inherit entity scope
|
|
218
|
+
(`privacyTier: undefined`); never auto-upgrade. The renderer helper
|
|
219
|
+
centralizes filtering; even if a Phase 4 callsite forgets to filter,
|
|
220
|
+
the helper's existence + spec-mandated call pattern catches the bug
|
|
221
|
+
at code-review time.
|
|
222
|
+
|
|
223
|
+
## 7. Rollback cost
|
|
224
|
+
|
|
225
|
+
- **Hot-fix release**: Pure additive change. `git revert <merge-commit>`
|
|
226
|
+
ships as the next patch.
|
|
227
|
+
- Backfilled `entity_evidence` rows from the `manual` producer would
|
|
228
|
+
remain in the DB; they are well-formed and harmless (renderer hides
|
|
229
|
+
them per viewer scope, never auto-fetched). If cleanup is desired,
|
|
230
|
+
`DELETE FROM entity_evidence WHERE kind = 'external-url'
|
|
231
|
+
AND note = 'Backfilled from legacy MemoryEntity.source (Phase 5 migration)'`
|
|
232
|
+
is a precise rollback query.
|
|
233
|
+
- `EvidenceRenderer.ts` deletion is fine; `SemanticMemory` would need
|
|
234
|
+
to re-introduce the local predicates. The revert PR can re-add them.
|
|
235
|
+
- **Data migration**: None. Backfill is one-shot per entity by design;
|
|
236
|
+
rollback doesn't undo the writes (they're treated as legitimate
|
|
237
|
+
evidence rows), and re-running after revert+restore would just
|
|
238
|
+
produce zero writes thanks to idempotency.
|
|
239
|
+
- **Agent state repair**: None.
|
|
240
|
+
- **User visibility**: Zero today (no HTTP / dashboard renderers Phase 4
|
|
241
|
+
hasn't shipped); minimal after Phase 4 lands (filtered evidence rows
|
|
242
|
+
appear next to entities).
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Conclusion
|
|
247
|
+
|
|
248
|
+
WikiClaim Phase 5 ships an idempotent, dry-run-safe, URL-only backfill
|
|
249
|
+
CLI plus the renderer helper that closes the privacy-enforcement loop the
|
|
250
|
+
spec § Storage and Privacy line 315 calls for. The CLI's intentional
|
|
251
|
+
narrowness (URLs only, never `session:` / `user:`) preserves the
|
|
252
|
+
`PRODUCER_KIND_ALLOWLIST` boundary Phase 1 installed; widening for
|
|
253
|
+
migration convenience would unwind a structural defense. The renderer
|
|
254
|
+
helper is preventive — Phase 4 HTTP/dashboard hasn't landed yet, so
|
|
255
|
+
there are zero ad-hoc renderers to replace today, but the helper is in
|
|
256
|
+
place and `SemanticMemory`'s own read paths now delegate to it so the
|
|
257
|
+
codebase has exactly one visibility-comparison map. Cleared to ship
|
|
258
|
+
pending second-pass concurrence.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Second-pass review
|
|
263
|
+
|
|
264
|
+
_To be filled in by independent reviewer subagent._
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Evidence pointers
|
|
269
|
+
|
|
270
|
+
- New tests:
|
|
271
|
+
`npx vitest run tests/unit/memory-backfill-evidence.test.ts tests/unit/evidence-render-privacy.test.ts`
|
|
272
|
+
→ 34/34 passing (~80ms).
|
|
273
|
+
- Regression:
|
|
274
|
+
`npx vitest run tests/unit/semantic-memory-evidence.test.ts tests/unit/semantic-memory.test.ts tests/unit/semantic-memory-privacy.test.ts`
|
|
275
|
+
→ 102/102 passing.
|
|
276
|
+
- Typecheck: `npx tsc --noEmit` → clean.
|
|
277
|
+
- Cross-product privacy coverage:
|
|
278
|
+
`tests/unit/evidence-render-privacy.test.ts` — 3 entity scopes × 5
|
|
279
|
+
evidence tiers × 3 viewer scopes (45 combinations) + edge-case
|
|
280
|
+
assertions.
|
|
281
|
+
- Spec source of truth:
|
|
282
|
+
`docs/specs/OPENCLAW-IMPORT-WIKICLAIM-EVIDENCE-SPEC.md` §§ Migration
|
|
283
|
+
of Existing MemoryEntity Records (line 202), Risks lines 357, 360,
|
|
284
|
+
Migration Plan Phase 5 (line 347), Storage and Privacy line 315.
|
|
285
|
+
- Phase 1 artifact: `upgrades/side-effects/wikiclaim-evidence-phase1.md`.
|