adolf: bearer-authenticate the agap MCP server, fix audio config schema
openclaw.json now sends `Authorization: Bearer ${AGAP_MCP_TOKEN}` to the agap
MCP server, which requires it as of kb#180. The token is injected from
openai/.env via docker-compose.yml and only substituted here, never inlined.
It maps to agent id `adolf`, which is also what the kb#147 vault gate reads.
Fixes tools.media.audio, which had been added but never restart-validated:
the per-entry `apiKey: "not-needed"` is rejected by the schema
("tools.media.audio.models.0: Invalid input"), and an invalid config makes the
gateway refuse to start outright -- adolf crash-looped on the first restart
after the block landed. The old comment claimed the schema requires a
non-empty apiKey; it is the opposite, apiKey is not a valid per-entry key at
all. Isolated with `openclaw config validate` against the running image
(2026.6.11): {provider, model} and {provider, model, baseUrl} validate, and
adding apiKey alone reproduces the failure. baseUrl is kept -- that is the
per-entry override pointing the openai-shaped provider at the local
faster-whisper server. Provider auth follows the normal model auth order per
docs/nodes/audio.md, and faster-whisper-server has no auth to satisfy anyway.
Two lessons encoded in the comments: `enabled: false` does NOT exempt an entry
from schema validation, and a config edit is not done until a restart boots
healthy -- this sat invalid but latent because the running gateway still held
an older loaded config. The block stays enabled: false; turning STT on is
still a kb#175/#191 decision (GTX 1070 co-residency).
Also adds the proactive-prioritization and todoist-capture design notes and
the vw-mcp prototype.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
345
adolf/DESIGN-proactive-prioritization.md
Normal file
345
adolf/DESIGN-proactive-prioritization.md
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
# DESIGN — Proactive action impact/cost prioritization (kb#123)
|
||||||
|
|
||||||
|
Status: **draft, for review** — written 2026-07-22
|
||||||
|
Owner: alvis · Written with Claude
|
||||||
|
Scope: kanboard Adolf task **#123**. This is a design/ruleset only — no wiring
|
||||||
|
into `openclaw.json`, no code. Claude implements it in a follow-up task.
|
||||||
|
|
||||||
|
Related, not duplicated here:
|
||||||
|
- **#122** (utility/ROI metrics via Langfuse) — that's the after-the-fact
|
||||||
|
"was Adolf worth it" readout across all of Adolf's spend. This doc is the
|
||||||
|
**before-the-fact gate** on one specific class of spend: proactive sends.
|
||||||
|
#122's Langfuse data is a candidate future input to the acceptance-rate term
|
||||||
|
below (§3.3), but this design does not depend on #122 landing first.
|
||||||
|
- **#124** (proactive cadence / cron schedule) — decides **when Adolf looks**
|
||||||
|
(daily/weekly/monthly audit cadence, adapted to quota). This design decides
|
||||||
|
**whether a specific candidate action fires** once #124 (or an ad-hoc
|
||||||
|
trigger, e.g. reacting to a calendar change) has already produced one.
|
||||||
|
#124 is the scheduler; this is the gate every candidate passes through
|
||||||
|
regardless of what triggered it.
|
||||||
|
- **#125** (feedback loop) — this design's acceptance-probability term (§3.3)
|
||||||
|
is a **consumer** of #125's feedback log. #125 is not built yet; §5 below
|
||||||
|
specifies exactly what it needs to log, as a dependency, not an assumption.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Problem
|
||||||
|
|
||||||
|
Adolf can generate a proactive message (reminder, nudge, digest item) from
|
||||||
|
several places: the cadence jobs in #124, a reactive trigger (calendar event
|
||||||
|
changed, HA sensor fired, a task went overdue), or background reasoning
|
||||||
|
noticing something. Not every candidate should be sent — some are low-value,
|
||||||
|
some are redundant with something already said, some cost real Kimi quota to
|
||||||
|
formulate and the window is nearly exhausted.
|
||||||
|
|
||||||
|
This design is a **gate function**: given a candidate proactive action, decide
|
||||||
|
fire / suppress / defer, using a score computed from four inputs the task
|
||||||
|
description names, plus a threshold that scales with remaining quota.
|
||||||
|
|
||||||
|
## 2. Where the gate sits
|
||||||
|
|
||||||
|
```
|
||||||
|
[trigger: #124 cadence job | reactive event | background noticing]
|
||||||
|
|
|
||||||
|
v
|
||||||
|
candidate proactive action
|
||||||
|
(draft content + metadata)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ IMPACT/COST GATE │ <-- this design
|
||||||
|
│ (score, threshold) │
|
||||||
|
└─────────────────────────┘
|
||||||
|
| |
|
||||||
|
fire suppress / defer
|
||||||
|
| |
|
||||||
|
send via Matrix log decision + reason
|
||||||
|
log outcome slot (no send; feedback loop
|
||||||
|
for #125 has nothing to attach to)
|
||||||
|
```
|
||||||
|
|
||||||
|
The gate is a pure function of the candidate + current state. It does not
|
||||||
|
decide *what* to consider sending (#124's job) or *how* to learn from
|
||||||
|
responses (#125's job) — only whether a given candidate clears the bar right
|
||||||
|
now.
|
||||||
|
|
||||||
|
## 3. Scoring model
|
||||||
|
|
||||||
|
For each candidate action `a`, compute:
|
||||||
|
|
||||||
|
```
|
||||||
|
score(a) = (benefit(a) * accept_prob(a) * urgency(a)) / cost(a)
|
||||||
|
```
|
||||||
|
|
||||||
|
Ratio form, not a weighted sum: cost is a genuine denominator (token cost is
|
||||||
|
literally what you're trading against benefit), and the three numerator terms
|
||||||
|
are gates on each other, not additive alternatives — a high-benefit,
|
||||||
|
low-acceptance-probability action *should* be suppressed even if urgency is
|
||||||
|
high, not partially compensated the way a sum would allow. All four terms are
|
||||||
|
normalized to comparable ranges as defined below so no single term dominates
|
||||||
|
by scale alone.
|
||||||
|
|
||||||
|
### 3.1 Benefit — `benefit(a) ∈ [0, 1]`
|
||||||
|
|
||||||
|
"How important is this to the user, if accepted." Estimated by Adolf itself
|
||||||
|
(the model doing the drafting) using a small fixed rubric — this is a
|
||||||
|
judgment call, not a measurement, so keep the rubric coarse enough to be
|
||||||
|
stable across repeated runs:
|
||||||
|
|
||||||
|
| Band | Value | Examples |
|
||||||
|
|---|---|---|
|
||||||
|
| Critical | 1.0 | Hard deadline today/tomorrow, safety/health-adjacent, financial penalty if missed |
|
||||||
|
| High | 0.7 | Real deadline this week, blocks another person, irreversible if missed |
|
||||||
|
| Medium | 0.4 | Useful reminder, no hard deadline, low cost if ignored |
|
||||||
|
| Low | 0.15 | "Might be nice to know", trivia-adjacent, no consequence |
|
||||||
|
|
||||||
|
Adolf assigns the band as part of drafting the candidate (one extra field in
|
||||||
|
the same generation pass — no separate LLM call). This is inherently noisy;
|
||||||
|
it is corrected over time by the acceptance-probability term (§3.3), which is
|
||||||
|
grounded in actual logged outcomes rather than self-assessment.
|
||||||
|
|
||||||
|
### 3.2 Cost — `cost(a) ∈ (0, 1]`, token cost normalized
|
||||||
|
|
||||||
|
Raw cost is estimable *before* sending: `tokens_estimate(a)` = prompt tokens
|
||||||
|
to formulate (system + context already loaded for the turn, since it's
|
||||||
|
piggy-backing on an existing generation) + estimated output tokens for the
|
||||||
|
message itself. For a message that requires its **own** dedicated Kimi call
|
||||||
|
(true incremental cost) vs. one riding along inside an already-scheduled
|
||||||
|
cadence turn (near-zero marginal cost), these are very different costs — the
|
||||||
|
estimate must distinguish "marginal call I wouldn't otherwise make" from
|
||||||
|
"free byproduct of a call happening anyway":
|
||||||
|
|
||||||
|
```
|
||||||
|
cost_tokens(a) = marginal_prompt_tokens(a) + marginal_output_tokens(a)
|
||||||
|
```
|
||||||
|
|
||||||
|
where `marginal_*` is 0 (or near-0, e.g. a few output tokens) if the action
|
||||||
|
rides inside a scheduled #124 audit turn that would run regardless, and the
|
||||||
|
full call cost if it requires spinning up a fresh Kimi turn.
|
||||||
|
|
||||||
|
Normalize against a reference ceiling (a "typical expensive proactive send",
|
||||||
|
empirically ~2-3K tokens per the #122 baseline measurements of ~32.8K for a
|
||||||
|
full reply turn — a standalone proactive nudge should be far cheaper than a
|
||||||
|
full conversational turn, since it's one-directional with no back-and-forth):
|
||||||
|
|
||||||
|
```
|
||||||
|
cost(a) = clamp(cost_tokens(a) / COST_CEILING, floor=0.05, cap=1.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
`COST_CEILING` = 3000 tokens (tunable constant, revisit once #122 gives real
|
||||||
|
distributions). The 0.05 floor stops a literally-free riding action from
|
||||||
|
dividing by ~0 and producing a runaway score — even "free" actions carry some
|
||||||
|
opportunity cost (attention, message-count against the 60/5h ceiling, not
|
||||||
|
just tokens).
|
||||||
|
|
||||||
|
### 3.3 Acceptance probability — `accept_prob(a) ∈ [0, 1]`
|
||||||
|
|
||||||
|
**This term has no data source yet.** It depends entirely on #125 (feedback
|
||||||
|
loop) being built and logging outcomes. Until then, use a flat prior:
|
||||||
|
|
||||||
|
```
|
||||||
|
accept_prob(a) = 0.5 # uninformative prior, pending #125
|
||||||
|
```
|
||||||
|
|
||||||
|
Once #125 logs `(action_class, outcome)` pairs (see §5's exact schema
|
||||||
|
requirement — this design does not invent history, it specifies what must
|
||||||
|
exist), compute a per-class empirical rate with Laplace smoothing so a class
|
||||||
|
with zero or few samples doesn't overfit to noise:
|
||||||
|
|
||||||
|
```
|
||||||
|
accept_prob(class) = (accepted_count(class) + 1) / (total_count(class) + 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
`class` is a coarse bucket, not per-message: e.g. `{calendar_reminder,
|
||||||
|
task_overdue, ha_anomaly, family_wiki_gap, digest_item, ...}` — one row per
|
||||||
|
class, not per exact message text, since exact-text history rarely repeats
|
||||||
|
but the class does. A candidate's class is assigned at draft time (same pass
|
||||||
|
as §3.1's benefit band).
|
||||||
|
|
||||||
|
Recency matters more than total count — a user who started dismissing
|
||||||
|
`calendar_reminder` last week should pull that class's rate down faster than
|
||||||
|
five-year-old acceptances prop it up. Use a decayed count (e.g. half-life of
|
||||||
|
30 days, or simply windowing to the trailing N=50 outcomes per class) rather
|
||||||
|
than an all-time average, once enough volume exists to make decay
|
||||||
|
meaningful.
|
||||||
|
|
||||||
|
### 3.4 Urgency — `urgency(a) ∈ [0, 1]`
|
||||||
|
|
||||||
|
Distinct from benefit: benefit is "how much it matters", urgency is "how soon
|
||||||
|
it stops being actionable". A time-decay curve against the nearest relevant
|
||||||
|
deadline (`due_at`) known for the candidate (calendar event start, task
|
||||||
|
`date_due`, HA-derived risk window):
|
||||||
|
|
||||||
|
```
|
||||||
|
hours_to_deadline = (due_at - now) in hours
|
||||||
|
|
||||||
|
urgency(a) =
|
||||||
|
1.0 if hours_to_deadline <= 1
|
||||||
|
1.0 - 0.6 * (h - 1) / 23 if 1 < h <= 24 (1.0 -> 0.4 over the day)
|
||||||
|
0.4 * exp(-(h - 24) / 168) if h > 24 (decays over the following week)
|
||||||
|
0.2 if no deadline (informational-only action)
|
||||||
|
```
|
||||||
|
|
||||||
|
Concretely: something due within the hour scores 1.0, something due
|
||||||
|
tomorrow ~0.4-1.0 depending on how close, something a week out trails off
|
||||||
|
toward the 0.2 floor for undated nudges. This is a simple monotonic decay, not
|
||||||
|
a precise model — tune the constants once real cadence data exists (#124).
|
||||||
|
|
||||||
|
### 3.5 Putting it together
|
||||||
|
|
||||||
|
```
|
||||||
|
score(a) = (benefit(a) * accept_prob(a) * urgency(a)) / cost(a)
|
||||||
|
```
|
||||||
|
|
||||||
|
Range: numerator ∈ [0, 1], denominator ∈ [0.05, 1], so
|
||||||
|
`score(a) ∈ [0, 20]` in the degenerate cheapest/most-urgent/most-beneficial
|
||||||
|
case. In practice, typical scores cluster well below that ceiling — the
|
||||||
|
threshold (§4) is calibrated empirically against observed scores, not derived
|
||||||
|
analytically from the range.
|
||||||
|
|
||||||
|
## 4. Firing rule
|
||||||
|
|
||||||
|
```
|
||||||
|
fire(a) iff score(a) >= threshold(current_quota_state)
|
||||||
|
else suppress(a) # or defer(a), see below
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.1 Quota signal
|
||||||
|
|
||||||
|
Read `adolf-llm:8010/usage` (confirmed live shape, sampled 2026-07-22):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"weekly": {"pct": 32, "used": 32, "limit": 100, "remaining": 68, "resets": "..."},
|
||||||
|
"window_5h":{"pct": 79, "used": 79, "limit": 100, "remaining": 21, "resets": "..."},
|
||||||
|
"stale": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the **tighter** of the two windows — whichever pct is higher is the
|
||||||
|
binding constraint right now:
|
||||||
|
|
||||||
|
```
|
||||||
|
quota_pressure = max(weekly.pct, window_5h.pct) / 100 # ∈ [0, 1]
|
||||||
|
```
|
||||||
|
|
||||||
|
If `stale: true` (Kimi login/session broken, per the adolf-llm fallback
|
||||||
|
behavior), treat as `quota_pressure = 1.0` (most conservative) — an unknown
|
||||||
|
quota state should suppress non-critical sends, not fire them.
|
||||||
|
|
||||||
|
### 4.2 Threshold as a function of quota pressure
|
||||||
|
|
||||||
|
```
|
||||||
|
threshold(quota_pressure) = T_BASE + (T_MAX - T_BASE) * quota_pressure^2
|
||||||
|
```
|
||||||
|
|
||||||
|
- `T_BASE` = 0.3 — threshold when quota is abundant (pressure ~0): let most
|
||||||
|
medium-benefit things through.
|
||||||
|
- `T_MAX` = 3.0 — threshold when quota is nearly exhausted (pressure ~1):
|
||||||
|
only near-maximal score (critical benefit, high acceptance history, urgent,
|
||||||
|
cheap) still fires.
|
||||||
|
- Squaring `quota_pressure` keeps the threshold flat and permissive through
|
||||||
|
low-to-mid pressure (nothing changes until quota actually gets tight) and
|
||||||
|
then rises steeply as the window approaches exhaustion — matching the
|
||||||
|
actual failure mode (403 usage-limit) which is a cliff, not a slope.
|
||||||
|
|
||||||
|
This gives a single tunable curve with two constants, both revisitable once
|
||||||
|
#122 supplies real score distributions and false-negative/positive rates.
|
||||||
|
|
||||||
|
### 4.3 Fire / suppress / defer
|
||||||
|
|
||||||
|
- **fire**: send now.
|
||||||
|
- **suppress**: below threshold and no deadline pressure — drop it. Log the
|
||||||
|
decision (§5) but do not re-surface it later; if it's still relevant, the
|
||||||
|
next cadence pass (#124) will regenerate it as a fresh candidate with
|
||||||
|
updated urgency.
|
||||||
|
- **defer**: below threshold *only because of quota pressure*, but
|
||||||
|
`urgency(a) >= 0.8` (i.e., something time-critical got starved by a quota
|
||||||
|
cliff, not by low benefit). Requeue for immediate re-evaluation once
|
||||||
|
`quota_pressure` drops (next window reset, per `resets` timestamp in the
|
||||||
|
usage payload) rather than silently dropping it. This is the one exception
|
||||||
|
to "gate is stateless" — a deferred item carries state (its own candidate
|
||||||
|
record) until it either fires or its deadline passes, at which point it is
|
||||||
|
logged as a missed/expired suppression, not silently lost.
|
||||||
|
|
||||||
|
## 5. Dependency: what #125's feedback log must contain
|
||||||
|
|
||||||
|
This design's accept_prob term (§3.3) is inert without it. #125 owns building
|
||||||
|
the collection mechanism (reactions, "+/-/неактуально" replies); this design
|
||||||
|
only specifies the **shape** the gate needs to consume, so the two tasks
|
||||||
|
don't diverge on schema:
|
||||||
|
|
||||||
|
```
|
||||||
|
proactive_outcome {
|
||||||
|
action_class: string # matches the class taxonomy in §3.3, e.g. "calendar_reminder"
|
||||||
|
sent_at: timestamp
|
||||||
|
benefit_band: float # the benefit(a) value used at send time, for later calibration
|
||||||
|
cost_tokens: int # actual cost, for calibrating COST_CEILING
|
||||||
|
urgency_at_send: float
|
||||||
|
outcome: enum { accepted, dismissed, ignored, irrelevant }
|
||||||
|
responded_at: timestamp | null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`ignored` (no response within some window, e.g. 24h) must be distinguished
|
||||||
|
from `dismissed` (explicit "−") — an ignored item is weaker negative signal
|
||||||
|
than an explicit rejection and should decay the acceptance rate less
|
||||||
|
aggressively. Without this distinction the Laplace-smoothed rate in §3.3
|
||||||
|
conflates "user didn't care" with "user was just busy."
|
||||||
|
|
||||||
|
Every **suppressed** and **deferred** candidate should also be logged (not
|
||||||
|
just fired ones) with `outcome: not_sent` — this is what lets a later audit
|
||||||
|
(#122) compute false-suppression rate (was a suppressed item actually needed?
|
||||||
|
only knowable in hindsight, e.g. if the same underlying deadline later caused
|
||||||
|
a problem) as well as false-fire rate.
|
||||||
|
|
||||||
|
## 6. Worked example
|
||||||
|
|
||||||
|
Candidate: "reminder that the Seafile SSL cert renews in 3 days" (from a
|
||||||
|
#124 daily cadence audit, riding along inside that scheduled call).
|
||||||
|
|
||||||
|
- `benefit`: Medium band → 0.4 (annoying if missed, not critical — auto-renew
|
||||||
|
likely already configured, this is a check not a fire drill).
|
||||||
|
- `cost`: marginal — rides inside the already-running daily audit call, say
|
||||||
|
~150 marginal output tokens → `150/3000 = 0.05` → floored at 0.05.
|
||||||
|
- `accept_prob`: no #125 data yet → flat prior 0.5.
|
||||||
|
- `urgency`: `hours_to_deadline` = 72h → falls in the `h > 24` branch:
|
||||||
|
`0.4 * exp(-(72-24)/168) = 0.4 * exp(-0.286) ≈ 0.4 * 0.751 ≈ 0.30`.
|
||||||
|
|
||||||
|
`score = (0.4 * 0.5 * 0.30) / 0.05 = 0.06 / 0.05 = 1.2`
|
||||||
|
|
||||||
|
At `quota_pressure = 0` (abundant quota), `threshold = 0.3` → **1.2 ≥ 0.3,
|
||||||
|
fires.** At `quota_pressure = 1` (window nearly exhausted, matching the
|
||||||
|
measured 79% 5h-window sample above, rounding up toward the cliff),
|
||||||
|
`threshold = 3.0` → **1.2 < 3.0, suppressed** (not deferred: urgency 0.30 is
|
||||||
|
well under the 0.8 defer bar) — correctly deprioritized under quota pressure
|
||||||
|
in favor of anything more urgent or already proven to land well.
|
||||||
|
|
||||||
|
## 7. Open parameters to tune post-implementation
|
||||||
|
|
||||||
|
Everything with a concrete numeric constant above (`COST_CEILING`, `T_BASE`,
|
||||||
|
`T_MAX`, the urgency decay constants, the defer bar) is a starting guess
|
||||||
|
consistent with the measurements already on hand (#122's token baseline, the
|
||||||
|
live `/usage` sample). None of it is load-bearing on the *shape* of the
|
||||||
|
model — only on where the dial sits. Revisit once:
|
||||||
|
|
||||||
|
- #125 supplies real `accept_prob` data (replacing the flat 0.5 prior is the
|
||||||
|
single highest-value follow-up — everything else is a reasonable guess,
|
||||||
|
this term is currently a placeholder).
|
||||||
|
- #122's Langfuse integration supplies real per-action token costs to
|
||||||
|
recalibrate `COST_CEILING`.
|
||||||
|
- A few weeks of fire/suppress/defer logs (§5) exist to check the threshold
|
||||||
|
isn't systematically over- or under-firing.
|
||||||
|
|
||||||
|
## 8. Acceptance check against kb#123
|
||||||
|
|
||||||
|
- Scoring formula with each term defined, ranged, and its estimation method
|
||||||
|
stated: §3.
|
||||||
|
- Firing rule (fire only if impact/cost exceeds a threshold): §4.
|
||||||
|
- Threshold tunable to current quota, against the real `adolf-llm:8010/usage`
|
||||||
|
signal: §4.1-4.2.
|
||||||
|
- Acceptance-probability term flagged as dependent on unbuilt history
|
||||||
|
(#125), with the exact log schema it needs specified rather than
|
||||||
|
fabricated: §3.3, §5.
|
||||||
|
- Cross-references to sibling tasks #122, #124, #125 without duplicating
|
||||||
|
their scope: header + inline.
|
||||||
237
adolf/DESIGN-todoist-capture.md
Normal file
237
adolf/DESIGN-todoist-capture.md
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
# DESIGN — Todoist capture + AI classification (kb#170)
|
||||||
|
|
||||||
|
Status: **v1, components 1/2 built + proven; components 3/4 designed, not
|
||||||
|
built** — written 2026-07-23
|
||||||
|
Owner: alvis · Written with Claude
|
||||||
|
Scope: kanboard Adolf task **#170**. Related, not duplicated: Welfare
|
||||||
|
#102 (proactive-secretary umbrella), #105 (idea capture, pre-Todoist
|
||||||
|
version of the same need), #106 (people/events reminders).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Why Todoist, and why encoder-only classification
|
||||||
|
|
||||||
|
Todoist becomes Adolf's **inbox for ideas and quick tasks** — the place a
|
||||||
|
thought gets captured immediately, before it's clear whether it's a
|
||||||
|
one-liner or a project. Two things must stay true per
|
||||||
|
`DESIGN-a2a-agents.md` v2.1:
|
||||||
|
|
||||||
|
- **No metered API by default** (§3a): classification must not spend a
|
||||||
|
Kimi/gemma turn per capture. The stack already keeps **bge-m3** resident
|
||||||
|
(Hindsight's embedder, never-evict, `model-registry.yaml`) — reusing it
|
||||||
|
for classification is ~0 marginal cost, same reasoning §3a already
|
||||||
|
applies to LiteLLM's Auto Router.
|
||||||
|
- **Native commands run before the agent** (proven pattern:
|
||||||
|
`quota-command-openclaw-plugin`, kb#62): a `/idea` command that never
|
||||||
|
invokes Kimi at all keeps the entire capture path — not just the
|
||||||
|
classification step — off the metered/quota-gated path.
|
||||||
|
|
||||||
|
So: **nearest-centroid classification over bge-m3 embeddings**, not a
|
||||||
|
classifier LLM call and not hard tag rules. This is genuinely an
|
||||||
|
encoder-only model in the literal sense (bge-m3 is an encoder, not a
|
||||||
|
generative LLM) — no fine-tuning, no training loop, because **no labelled
|
||||||
|
dataset exists** (inventing one would be guessing scope that wasn't
|
||||||
|
asked for). The "training data" is a small, git-editable exemplar list per
|
||||||
|
class (`agap-mcp/src/classifier.js`) — extending accuracy later means
|
||||||
|
adding exemplars, not retraining.
|
||||||
|
|
||||||
|
## 2. Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Matrix "/idea <текст>" Adolf prose ("запомни идею...")
|
||||||
|
| |
|
||||||
|
v v
|
||||||
|
todoist-capture-plugin Adolf (Kimi) -> MCP tool call
|
||||||
|
(native command, 0 Kimi calls) todoist_capture_idea (agap-mcp)
|
||||||
|
| |
|
||||||
|
+--------------------+-------------------+
|
||||||
|
v
|
||||||
|
POST /capture-idea (agap-mcp, plain REST)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
classifier.js: embed(text) via bge-m3
|
||||||
|
(1 embedding call, reused for all 3 axes)
|
||||||
|
|
|
||||||
|
+----------------+----------------+
|
||||||
|
v v v
|
||||||
|
area centroid urgency centroid decompose centroid
|
||||||
|
(5 classes) (3 classes) (2 classes)
|
||||||
|
| | |
|
||||||
|
+----------------+------------------+
|
||||||
|
v
|
||||||
|
capture.js: label + priority mapping
|
||||||
|
|
|
||||||
|
v
|
||||||
|
todoistCreateTask() -> real Todoist task
|
||||||
|
labels: area-*, urgency-*, [decompose], [area-uncertain]
|
||||||
|
```
|
||||||
|
|
||||||
|
Two entry points converge on one pipeline (`capture.js`'s
|
||||||
|
`todoistCaptureIdea`), reachable either as an MCP tool
|
||||||
|
(`todoist_capture_idea`, for Adolf's/Claude's model-driven path — "запомни
|
||||||
|
идею: ...") or as a plain REST route (`POST /capture-idea`, for the native
|
||||||
|
`/idea` command, which cannot speak MCP JSON-RPC). Both call the exact
|
||||||
|
same function — no duplicated classification/creation logic.
|
||||||
|
|
||||||
|
## 3. Component 2 — AI classification (built, proven)
|
||||||
|
|
||||||
|
Three independent axes per idea, one bge-m3 embedding shared across all
|
||||||
|
three:
|
||||||
|
|
||||||
|
| Axis | Classes | Source of exemplars |
|
||||||
|
|---|---|---|
|
||||||
|
| **area** | `adolf`, `welfare`, `дом`, `семья`, `здоровье` (kb#170 spec, verbatim) | `AREA_EXEMPLARS` |
|
||||||
|
| **urgency** | `high`, `medium`, `low` | `URGENCY_EXEMPLARS` |
|
||||||
|
| **decompose** | `simple-task`, `needs-decomposition` | `DECOMPOSE_EXEMPLARS` |
|
||||||
|
|
||||||
|
Classification = cosine similarity of the idea's embedding against each
|
||||||
|
class's centroid (mean of that class's exemplar embeddings), argmax per
|
||||||
|
axis. Each result also carries a **margin** (gap between the top two
|
||||||
|
scores) and an `ambiguous: true` flag when the margin is small
|
||||||
|
(< 0.03, an empirical starting threshold — same "tune later" posture as
|
||||||
|
`DESIGN-proactive-prioritization.md`'s constants). Ambiguous area
|
||||||
|
classifications get an extra `area-uncertain` label instead of being
|
||||||
|
silently forced — component 4 (periodic review) is where a human
|
||||||
|
resolves them, not an auto-retry on a bigger model (consistent with
|
||||||
|
`DESIGN-a2a-agents.md` §5's always-ask escalation policy, scaled down:
|
||||||
|
this isn't a costly/irreversible action, so the "escalation" here is just
|
||||||
|
a label, not a blocking gate to alvis's inbox).
|
||||||
|
|
||||||
|
**Proven** (`agap-mcp/src/classifier.test.mjs`, run against the real,
|
||||||
|
live bge-m3 at `:11436` — 7/7 pass): area/urgency/decompose all resolve
|
||||||
|
sensibly on hand-written Russian idea text spanning all 5 areas, both
|
||||||
|
urgency bands, and both decompose classes. `capture.test.mjs` (6/6 pass)
|
||||||
|
proves the label/priority/project mapping with a **stubbed** Todoist
|
||||||
|
client — no test data was written to the real Todoist account while
|
||||||
|
proving this out.
|
||||||
|
|
||||||
|
### 3.1 Project vs. label mapping — a decision made, not guessed
|
||||||
|
|
||||||
|
Todoist's real, live projects today (`todoist_list_projects`, confirmed
|
||||||
|
2026-07-23): `Inbox`, `One-Off`, `Family`, `Planning`, `Pending`. These do
|
||||||
|
**not** line up with the 5 kb#170 areas except `семья` ≈ `Family`.
|
||||||
|
Creating four new Todoist projects (`Adolf`, `Welfare`, `дом`,
|
||||||
|
`здоровье`) to match would be a structural change to the user's real
|
||||||
|
Todoist account — **not done here without sign-off** (see §6, open
|
||||||
|
question 1). Instead, v1 uses **labels** (`area-*`, `urgency-*`,
|
||||||
|
`decompose`, `area-uncertain`) for every axis — purely additive and
|
||||||
|
reversible (Todoist auto-creates labels on first use; deleting a label
|
||||||
|
loses no task data) — and auto-routes to an existing project only for the
|
||||||
|
one unambiguous match (`семья` → `Family`), never inventing a project
|
||||||
|
selection the classifier merely guessed at.
|
||||||
|
|
||||||
|
## 4. Component 1 — capture command (built, not activated)
|
||||||
|
|
||||||
|
`openai/todoist-capture-plugin/` — same shape as `quota-command-openclaw-
|
||||||
|
plugin` (kb#62): `definePluginEntry` + `api.registerCommand({ name:
|
||||||
|
"idea", acceptsArgs: true, requireAuth: true, handler })`. Verified
|
||||||
|
against the real `PluginCommandHandler`/`PluginCommandContext`/
|
||||||
|
`OpenClawPluginCommandDefinition` types in the OpenClaw source
|
||||||
|
(`/home/alvis/adolf/src/plugins/types.ts`) — `ctx.args` is the raw string
|
||||||
|
after `/idea`, handler returns `{ text, suppressReply }`.
|
||||||
|
|
||||||
|
`requireAuth: true` (default) keeps it behind the same Matrix DM allowlist
|
||||||
|
(`channels.matrix.dm.allowFrom`) gating every other Adolf interaction — no
|
||||||
|
new privilege tier, since creating a Todoist task in the operator's own
|
||||||
|
inbox isn't a privileged/destructive action.
|
||||||
|
|
||||||
|
**Wired but not live**: `docker-compose.yml` gets the read-only bind mount
|
||||||
|
(same pattern as `quota-command`/`hindsight-memory`/`kimi-quota-footer`),
|
||||||
|
`openclaw.json` gets `plugins.entries.todoist-capture.enabled: true`, and
|
||||||
|
`agap-mcp/src/server.js` gets the `POST /capture-idea` route the plugin
|
||||||
|
calls. All three are plain config/code edits, proven end-to-end with a
|
||||||
|
stub Todoist client (see kb#170 report) — but **none of this takes effect
|
||||||
|
until the adolf container is restarted** (same activation gate every prior
|
||||||
|
plugin in this repo has hit: bind-mounts and `plugins.entries` are read at
|
||||||
|
process start). That restart is the kb#170 handoff — see report.
|
||||||
|
|
||||||
|
## 5. Component 3 — sync with services (designed, not built)
|
||||||
|
|
||||||
|
kb#170's spec: "идеи из Todoist синхронизируются с Kanboard, календарём,
|
||||||
|
проектами." This is underspecified enough that building a concrete
|
||||||
|
bidirectional sync now would be guessing scope (direction? conflict
|
||||||
|
resolution? which Todoist state maps to which Kanboard column?) rather
|
||||||
|
than following it. **v1 proposal, one-way, human-gated — not built yet:**
|
||||||
|
|
||||||
|
- Todoist is the **source of truth for the idea itself** (text, labels,
|
||||||
|
done/not-done). Kanboard is the source of truth for **anything that
|
||||||
|
became real, tracked work**.
|
||||||
|
- Sync fires only from component 4's periodic review (§6), not on a
|
||||||
|
schedule or webhook: when Adolf proposes "this idea is ready to become
|
||||||
|
work" and the human agrees, Adolf creates **one** Kanboard task whose
|
||||||
|
description contains a `context ref` back to the Todoist task id (per
|
||||||
|
`DESIGN-a2a-agents.md` §2's "context travels by reference" rule — no
|
||||||
|
content duplication) and the Todoist task gets a `kanboard-<id>` label
|
||||||
|
and stays open until the Kanboard task closes.
|
||||||
|
- **No calendar sync is proposed in v1.** A Todoist due-date does not
|
||||||
|
imply a calendar event (most captured ideas won't have a real due
|
||||||
|
time), and the reverse (creating calendar entries from arbitrary idea
|
||||||
|
due-dates) risks cluttering Radicale with noise. Calendar involvement
|
||||||
|
belongs with Welfare #106's people/events reminders design, not
|
||||||
|
invented here.
|
||||||
|
- **No two-way Kanboard→Todoist sync.** Completing the Kanboard task does
|
||||||
|
not need to close the Todoist item automatically for v1 — a human
|
||||||
|
glancing at Todoist can see the `kanboard-<id>` label and close it
|
||||||
|
manually; automating that closure is a small, safe follow-up once the
|
||||||
|
one-way direction above is live and observed, not blocking v1.
|
||||||
|
|
||||||
|
This keeps sync a **consequence of the human-gated review** (§6), never
|
||||||
|
an autonomous background writer to three services at once — consistent
|
||||||
|
with `DESIGN-a2a-agents.md` §5's always-ask posture for anything crossing
|
||||||
|
a service boundary. Building this is out of scope for this pass; flagged
|
||||||
|
in the kb#170 report as a natural follow-up task once components 1/2 are
|
||||||
|
live and real capture data exists to review.
|
||||||
|
|
||||||
|
## 6. Component 4 — periodic review (designed, not built)
|
||||||
|
|
||||||
|
"Adolf предлагает, какие идеи созрели для превращения в задачи" — this is
|
||||||
|
explicitly Adolf proposing, not auto-converting; matches
|
||||||
|
`DESIGN-a2a-agents.md` §5's always-ask escalation policy exactly (a
|
||||||
|
decision task to alvis's inbox, not a silent action). Proposed shape,
|
||||||
|
**not implemented**:
|
||||||
|
|
||||||
|
- A low-priority proactive cadence job (same shape as Welfare #124's
|
||||||
|
cadence design, once that lands) that runs `todoist_list_tasks` scoped
|
||||||
|
to labels `decompose` or `area-uncertain`, drafts a short proposal per
|
||||||
|
candidate ("эта идея выглядит готовой к декомпозиции — завести задачу
|
||||||
|
в Kanboard?"), and sends it via Matrix — same impact/cost gate as
|
||||||
|
`DESIGN-proactive-prioritization.md` (kb#123) should apply here too,
|
||||||
|
once that gate exists, rather than inventing a second one.
|
||||||
|
- On accept: component 3's one-way sync (§5) fires for that one idea.
|
||||||
|
- On dismiss: the idea's `decompose`/`area-uncertain` label is cleared so
|
||||||
|
the same candidate doesn't re-surface every cadence run.
|
||||||
|
|
||||||
|
Not built now because it depends on #124 (cadence) and, for a well-
|
||||||
|
calibrated gate, #123 (impact/cost gate) — both cited as dependencies
|
||||||
|
rather than duplicated, same posture `DESIGN-proactive-prioritization.md`
|
||||||
|
itself takes toward its own siblings.
|
||||||
|
|
||||||
|
## 7. What's built vs. handed off
|
||||||
|
|
||||||
|
| Piece | State |
|
||||||
|
|---|---|
|
||||||
|
| `agap-mcp/src/classifier.js` + test | Built, proven live against bge-m3 |
|
||||||
|
| `agap-mcp/src/capture.js` + test | Built, proven with a stubbed Todoist client (no real writes) |
|
||||||
|
| `agap-mcp/src/server.js`: `todoist_capture_idea` MCP tool + `POST /capture-idea` | Built; **not live** — needs an agap-mcp rebuild/restart (already true of the whole Todoist tool surface per kb#170 orchestrator note) |
|
||||||
|
| `adolf/openclaw.json`, `openai/shared-mcp.json`, `openai/agent-registry.yaml` | Edited, `validate_capability_grants.py` passes clean at both layers |
|
||||||
|
| `openai/todoist-capture-plugin/` (`/idea` command) | Built, HTTP contract proven with a local harness; **not live** — needs the adolf container restarted (bind mount + `plugins.entries` already wired) |
|
||||||
|
| Component 3 (sync) | Designed (§5), not built — genuine scope decisions flagged, not guessed |
|
||||||
|
| Component 4 (periodic review) | Designed (§6), not built — depends on Welfare #123/#124 |
|
||||||
|
|
||||||
|
## 8. Open questions for alvis (not guessed)
|
||||||
|
|
||||||
|
1. **Todoist project structure**: keep the label-only v1 (§3.1), or
|
||||||
|
create dedicated Todoist projects per area? The latter is a real,
|
||||||
|
visible change to the account structure and needs explicit sign-off.
|
||||||
|
2. **Activation**: rebuilding agap-mcp (its own repo/compose,
|
||||||
|
`agap_git/agap-mcp/docker-compose.yml`, `build: .` — the `Dockerfile`
|
||||||
|
`COPY`s `src/` into the image, no bind mount, so a code change needs a
|
||||||
|
rebuild, not just a restart) and restarting the adolf container (its
|
||||||
|
compose is `agap_git/openai/docker-compose.yml`, to re-read
|
||||||
|
`openclaw.json` and pick up the new plugin bind mount) are the two
|
||||||
|
outward-facing steps this task deliberately did not take. Exact
|
||||||
|
commands, once approved:
|
||||||
|
```
|
||||||
|
cd /home/alvis/agap_git/agap-mcp && docker compose build && docker compose up -d
|
||||||
|
cd /home/alvis/agap_git/openai && docker compose up -d adolf
|
||||||
|
```
|
||||||
@@ -11,6 +11,16 @@ gateway configuration**.
|
|||||||
the `openai` compose project's own tree.
|
the `openai` compose project's own tree.
|
||||||
- Model backend: `adolf-llm` container (Kimi-CLI wrapper) on `:8010`
|
- Model backend: `adolf-llm` container (Kimi-CLI wrapper) on `:8010`
|
||||||
|
|
||||||
|
## Memory
|
||||||
|
|
||||||
|
Adolf's long-term memory is being **migrated from Cognee to Hindsight** (a single
|
||||||
|
self-hosted container, `:8888` REST + built-in MCP, `:9999` UI). It stays wired in
|
||||||
|
the same two ways: as a **tool** (Hindsight's built-in MCP in `openclaw.json`
|
||||||
|
`mcp.servers.hindsight`) and as **forced hooks** (the `hindsight-memory` OpenClaw
|
||||||
|
plugin: `before_prompt_build`→recall inject, `agent_end`→retain). Authoritative
|
||||||
|
plan and target architecture: **[`HINDSIGHT-MIGRATION.md`](./HINDSIGHT-MIGRATION.md)**
|
||||||
|
(kanboard *Adolf* tasks H1–H5). Until those land, the running stack is still Cognee.
|
||||||
|
|
||||||
## Config source of truth
|
## Config source of truth
|
||||||
|
|
||||||
The gateway config is **`openclaw.json` in this directory**. It is bind-mounted
|
The gateway config is **`openclaw.json` in this directory**. It is bind-mounted
|
||||||
@@ -109,3 +119,32 @@ To **revoke** access, remove the ID from `allowFrom` and restart.
|
|||||||
> Matrix accounts themselves are created on the Synapse homeserver
|
> Matrix accounts themselves are created on the Synapse homeserver
|
||||||
> (`mtx.alogins.net`) — see the AgapHost wiki **Matrix** page. The allow-list
|
> (`mtx.alogins.net`) — see the AgapHost wiki **Matrix** page. The allow-list
|
||||||
> here only controls which existing Matrix users Adolf will talk to.
|
> here only controls which existing Matrix users Adolf will talk to.
|
||||||
|
|
||||||
|
## Matrix device identity (kb#67)
|
||||||
|
|
||||||
|
Adolf's Matrix login used **password auth with no pinned `device_id`**
|
||||||
|
(`MATRIX_PASSWORD` in `openai/.env`). Every time OpenClaw's own credential
|
||||||
|
cache (in the `adolf-state` volume) was missing — first boot, a lost/rebuilt
|
||||||
|
volume — a fresh password login minted a **brand-new Matrix device** with no
|
||||||
|
cross-signing, leaving dead ghost devices behind and risking new encrypted
|
||||||
|
DMs getting keys shared to a device that no longer exists.
|
||||||
|
|
||||||
|
Fix: `openai/.env` now also pins `MATRIX_ACCESS_TOKEN` + `MATRIX_DEVICE_ID` to
|
||||||
|
Adolf's current live device (`TIANDTKUZJ`, cross-signed; token in Vaultwarden
|
||||||
|
as `MATRIX_ADOLF_GATEWAY_TOKEN`). OpenClaw's matrix extension prefers a
|
||||||
|
configured access token over password login
|
||||||
|
(`extensions/matrix/src/matrix/client/config.ts` `resolveMatrixAuth`), so as
|
||||||
|
long as that token stays valid, restarts — even after a volume loss — reuse
|
||||||
|
the same device instead of minting a new one. `MATRIX_PASSWORD` stays set as
|
||||||
|
a manual-recovery fallback only (unset `MATRIX_ACCESS_TOKEN` to force a fresh
|
||||||
|
password login if the token is ever revoked).
|
||||||
|
|
||||||
|
Cross-signing for `@bot` is already bootstrapped automatically by OpenClaw's
|
||||||
|
matrix extension (`extensions/matrix/src/matrix/sdk/crypto-bootstrap.ts`) —
|
||||||
|
no separate setup needed.
|
||||||
|
|
||||||
|
If the pinned token is ever revoked/rotated, get a fresh one bound to the
|
||||||
|
*same* device by logging in with `device_id` explicitly set to `TIANDTKUZJ`
|
||||||
|
(Synapse reuses an existing device when its id is given in `/login`, instead
|
||||||
|
of creating a new one), then update `MATRIX_ACCESS_TOKEN` in `openai/.env`
|
||||||
|
and Vaultwarden's `MATRIX_ADOLF_GATEWAY_TOKEN`.
|
||||||
|
|||||||
@@ -29,6 +29,71 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Audio understanding (kb#175, STT source 2: "local STT on Agap").
|
||||||
|
// OpenClaw's bundled media-understanding pipeline (docs/nodes/media-
|
||||||
|
// understanding.md) auto-transcribes inbound audio attachments before the
|
||||||
|
// agent turn runs -- no plugin code needed, this is pure config. A voice
|
||||||
|
// note sent to Adolf over the existing Matrix DM (source 1, see below)
|
||||||
|
// gets transcribed by the `openai`-shaped entry below, which is redirected
|
||||||
|
// via baseUrl/apiKey to the LOCAL faster-whisper server (openai/docker-
|
||||||
|
// compose.yml's `faster-whisper` service, same compose project as this
|
||||||
|
// container, reachable by service name) instead of hosted OpenAI --
|
||||||
|
// confirmed supported via src/media-understanding/runner.entries.ts's
|
||||||
|
// per-entry baseUrl override (docs/gateway/config-tools.md "Tools and
|
||||||
|
// custom providers"). apiKey is a dummy: faster-whisper-server has no
|
||||||
|
// auth, but OpenClaw's schema requires a non-empty value.
|
||||||
|
//
|
||||||
|
// ⚠️ NOT ACTIVATED YET (2026-07-26): the `faster-whisper` container does
|
||||||
|
// not exist (never started -- `docker ps -a` shows no such container).
|
||||||
|
// Starting it opens a 4th tenant on the single 8GB GTX 1070 already at
|
||||||
|
// ~6.2GB with bge-m3 + gemma3:4b + tei-reranker (~1.8GB headroom) and
|
||||||
|
// could evict tei-reranker, silently breaking Hindsight recall (see
|
||||||
|
// DESIGN-a2a-agents.md §3b and kb#191, which is the unimplemented
|
||||||
|
// residency-guard/VRAM-alert task -- still in Backlog). This config
|
||||||
|
// block is deliberately inert until #191 lands or alvis explicitly
|
||||||
|
// accepts the co-residency risk; `enabled: false` below.
|
||||||
|
// SCHEMA FIX 2026-07-30 (kb#180 restart). This block previously carried a
|
||||||
|
// per-entry `apiKey: "not-needed"`, which the schema REJECTS:
|
||||||
|
// tools.media.audio.models.0: Invalid input
|
||||||
|
// and an invalid config makes the gateway refuse to start outright -- adolf
|
||||||
|
// crash-looped on the first restart after the block was added. Two lessons
|
||||||
|
// encoded here: `enabled: false` does NOT exempt an entry from schema
|
||||||
|
// validation, and a config edit is not "done" until a real restart boots
|
||||||
|
// healthy (this block sat invalid but latent because the running gateway
|
||||||
|
// still held an older loaded config).
|
||||||
|
//
|
||||||
|
// The old comment claimed the schema "requires a non-empty apiKey" -- it is
|
||||||
|
// the opposite: `apiKey` is not a valid per-entry key at all. Verified with
|
||||||
|
// `openclaw config validate` against this image (2026.6.11): entries with
|
||||||
|
// {provider, model} and {provider, model, baseUrl} validate; adding
|
||||||
|
// `apiKey` is the sole cause of the failure. Per docs/nodes/audio.md,
|
||||||
|
// provider auth follows the normal model auth order (auth profiles, env
|
||||||
|
// vars, models.providers.*.apiKey) -- and faster-whisper-server has no auth
|
||||||
|
// to satisfy anyway, so no key belongs here. `baseUrl` is kept: that is the
|
||||||
|
// per-entry override that redirects the `openai`-shaped provider to the
|
||||||
|
// LOCAL faster-whisper server.
|
||||||
|
tools: {
|
||||||
|
media: {
|
||||||
|
audio: {
|
||||||
|
// Still deliberately inert: the `faster-whisper` container does not
|
||||||
|
// exist, and starting it opens a 4th tenant on the 8GB GTX 1070 (see
|
||||||
|
// the kb#191 residency-guard note above). This block is now merely
|
||||||
|
// SCHEMA-VALID rather than boot-breaking; flipping this to true is a
|
||||||
|
// separate decision that still belongs to kb#175/#191.
|
||||||
|
enabled: false,
|
||||||
|
echoTranscript: true, // let the sender see what Adolf heard before it acts
|
||||||
|
echoFormat: '📝 "{transcript}"',
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
provider: "openai",
|
||||||
|
model: "deepdml/faster-whisper-large-v3-turbo-ct2", // must match WHISPER__MODEL in openai/docker-compose.yml
|
||||||
|
baseUrl: "http://faster-whisper:8000/v1",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// Browser tool — bundled plugin, off by default. Enables a dedicated,
|
// Browser tool — bundled plugin, off by default. Enables a dedicated,
|
||||||
// agent-only headless Chromium profile ("openclaw") driven through the
|
// agent-only headless Chromium profile ("openclaw") driven through the
|
||||||
// gateway's loopback control service. Chromium is already in the image
|
// gateway's loopback control service. Chromium is already in the image
|
||||||
@@ -146,9 +211,27 @@
|
|||||||
// of which Adolf's Matrix persona drives turn-to-turn; reach the
|
// of which Adolf's Matrix persona drives turn-to-turn; reach the
|
||||||
// hindsight MCP directly (unscoped) for that admin work instead of
|
// hindsight MCP directly (unscoped) for that admin work instead of
|
||||||
// paying for it on every Adolf turn. 29 tools -> 9.
|
// paying for it on every Adolf turn. 29 tools -> 9.
|
||||||
|
//
|
||||||
|
// kb#169: this raw MCP surface previously pointed at /mcp/adolf/ --
|
||||||
|
// the single unpartitioned bank with content from every human's
|
||||||
|
// conversations. #153 scopes the hindsight-memory PLUGIN's
|
||||||
|
// recall/retain hooks by interlocutor, but this MCP tool surface is a
|
||||||
|
// second, independent path to memory that #153 does not touch: a
|
||||||
|
// model call to e.g. `recall` here bypassed interlocutor scoping
|
||||||
|
// entirely. Repointed to /mcp/adolf-shared/ (option 2 of #169) --
|
||||||
|
// the household-shared bank (0 facts as of 2026-07-26, pre-existing
|
||||||
|
// per agent-registry.yaml's memory.banks target list). This surface
|
||||||
|
// can now only ever read/write the shared bank, never a private one,
|
||||||
|
// regardless of which human is talking to Adolf -- safe by
|
||||||
|
// construction, no dependency on #153 landing first. Option 1 (drop
|
||||||
|
// entirely) was not chosen because, until #153's scoped plugin tools
|
||||||
|
// land, this is still Adolf's only path for explicit "remember
|
||||||
|
// this"/"what do you recall" turns; option 3 (dynamic per-interlocutor
|
||||||
|
// bank selection) is not supported -- this config's url is a single
|
||||||
|
// static bank_id per MCP server entry, not a per-request parameter.
|
||||||
hindsight: {
|
hindsight: {
|
||||||
type: "http",
|
type: "http",
|
||||||
url: "http://hindsight:8888/mcp/adolf/",
|
url: "http://hindsight:8888/mcp/adolf-shared/",
|
||||||
toolFilter: {
|
toolFilter: {
|
||||||
include: ["recall", "retain", "reflect", "list_memories", "get_memory", "update_memory", "list_directives", "create_directive", "delete_directive"],
|
include: ["recall", "retain", "reflect", "list_memories", "get_memory", "update_memory", "list_directives", "create_directive", "delete_directive"],
|
||||||
},
|
},
|
||||||
@@ -210,7 +293,7 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
// agap-mcp (kb#64) -- the SAME shared agap-mcp instance Claude Code uses
|
// agap-mcp (kb#64) -- the SAME shared agap-mcp instance Claude Code uses
|
||||||
// (network_mode: host, :3100, unauthenticated on localhost). Grants Adolf
|
// (network_mode: host, :3100, bearer-authenticated since kb#180). Grants Adolf
|
||||||
// the same access as Claude: vault (vw_* for credential fetching) plus
|
// the same access as Claude: vault (vw_* for credential fetching) plus
|
||||||
// gitea/ha/zabbix/radicale. Reached via host.docker.internal like
|
// gitea/ha/zabbix/radicale. Reached via host.docker.internal like
|
||||||
// kanboard/marketplace above.
|
// kanboard/marketplace above.
|
||||||
@@ -228,8 +311,24 @@
|
|||||||
agap: {
|
agap: {
|
||||||
type: "http",
|
type: "http",
|
||||||
url: "http://host.docker.internal:3100/mcp",
|
url: "http://host.docker.internal:3100/mcp",
|
||||||
|
// kb#180: agap-mcp's listener is authenticated now (DESIGN §4 --
|
||||||
|
// :3100 is host-networked and the LAN carries VPN peers, so an
|
||||||
|
// open JSON-RPC listener handed ha_call_service/wiki_edit/todoist
|
||||||
|
// writes to anyone). Same pattern as marketplace above:
|
||||||
|
// AGAP_MCP_TOKEN lives in Vaultwarden, is injected into this
|
||||||
|
// container via openai/.env -> docker-compose.yml, and is only
|
||||||
|
// substituted here -- never inlined. The token maps to agent id
|
||||||
|
// `adolf` in AGAP_MCP_AGENT_TOKENS, which is also what the kb#147
|
||||||
|
// vault gate reads to allow vw_* (adolf = trust_class trusted).
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer ${AGAP_MCP_TOKEN}",
|
||||||
|
},
|
||||||
toolFilter: {
|
toolFilter: {
|
||||||
include: ["vw_get_password", "vw_get_item", "vw_list_items", "vw_create_login", "vw_update_password", "ha_get_state", "ha_list_entities", "ha_call_service", "ha_get_history", "zabbix_get_problems", "zabbix_get_hosts", "zabbix_get_items", "zabbix_get_triggers", "radicale_list_calendars", "radicale_list_events", "radicale_get_event", "radicale_put_event", "radicale_delete_event", "radicale_move_event", "todoist_list_tasks", "todoist_list_projects", "todoist_create_task", "todoist_update_task", "todoist_complete_task", "wiki_search", "wiki_read", "wiki_edit"],
|
// kb#170: added todoist_capture_idea (classify + create in one
|
||||||
|
// call — see agap-mcp/src/capture.js) so Adolf's proactive-
|
||||||
|
// secretary persona can capture+tag an idea in one tool call
|
||||||
|
// instead of list_projects+create_task+manual tagging.
|
||||||
|
include: ["vw_get_password", "vw_get_item", "vw_list_items", "vw_create_login", "vw_update_password", "ha_get_state", "ha_list_entities", "ha_call_service", "ha_get_history", "zabbix_get_problems", "zabbix_get_hosts", "zabbix_get_items", "zabbix_get_triggers", "radicale_list_calendars", "radicale_list_events", "radicale_get_event", "radicale_put_event", "radicale_delete_event", "radicale_move_event", "todoist_list_tasks", "todoist_list_projects", "todoist_create_task", "todoist_update_task", "todoist_complete_task", "todoist_capture_idea", "wiki_search", "wiki_read", "wiki_edit"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -280,6 +379,15 @@
|
|||||||
"kimi-quota-footer": {
|
"kimi-quota-footer": {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
// Todoist idea capture (kb#170 component 1) — installed external
|
||||||
|
// plugin, bind-mounted read-only from openai/todoist-capture-plugin
|
||||||
|
// (see that project's docker-compose.yml adolf.volumes) onto
|
||||||
|
// .openclaw/extensions/todoist-capture. Registers a `/idea` native
|
||||||
|
// command; no hooks (no allowConversationAccess/allowPromptInjection
|
||||||
|
// needed) — it POSTs straight to agap-mcp's /capture-idea route.
|
||||||
|
"todoist-capture": {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
28
adolf/vw-mcp/.env.example
Normal file
28
adolf/vw-mcp/.env.example
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Copy to .env (git-ignored) and fill in real values before `docker compose up`.
|
||||||
|
# This server is intentionally narrow: read-only vw_* tools, a dedicated bot
|
||||||
|
# vault identity, and its own bearer token. See CLAUDE.md / kb task #64 for
|
||||||
|
# the full architecture and the sensitive setup steps (creating the bot user
|
||||||
|
# and the "Adolf" collection) that are NOT done by this scaffolding.
|
||||||
|
|
||||||
|
# Port this server listens on. 3100=agap-mcp, 3101=marketplace-mcp,
|
||||||
|
# 3103=kanboard-mcp, 3104=kanboard-mcp-adolf — 3105 verified free at write time.
|
||||||
|
PORT=3105
|
||||||
|
|
||||||
|
# Local Vaultwarden instance (NOT bitwarden.com). Unlike agap-mcp/
|
||||||
|
# marketplace-mcp, this service owns a fresh BITWARDENCLI_APPDATA_DIR volume
|
||||||
|
# with no pre-existing `bw config server`, so vaultwarden.js sets it on every
|
||||||
|
# boot from this var.
|
||||||
|
VW_URL=http://localhost:8041
|
||||||
|
|
||||||
|
# Dedicated bot identity — NEVER the master allogn@gmail.com account.
|
||||||
|
# Create this user in Vaultwarden first (sensitive step, reserved for the
|
||||||
|
# orchestrator — see report). Password: generate one and store it in
|
||||||
|
# Vaultwarden as item "ADOLF_VW_PASSWORD" (also a sensitive step).
|
||||||
|
BW_EMAIL=adolf-vault@auth.local
|
||||||
|
BW_PASSWORD=
|
||||||
|
|
||||||
|
# Bearer token gating /mcp, /sse, /messages (same pattern as
|
||||||
|
# marketplace-mcp). Generate with e.g. `openssl rand -hex 32`, store it in
|
||||||
|
# Vaultwarden as its own item (e.g. "VW_MCP_ADOLF_TOKEN"), and put the real
|
||||||
|
# value here — the line below is a PLACEHOLDER, not a usable secret.
|
||||||
|
VW_MCP_TOKEN=replace-with-output-of-openssl-rand--hex-32
|
||||||
2
adolf/vw-mcp/.gitignore
vendored
Normal file
2
adolf/vw-mcp/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.env
|
||||||
|
node_modules/
|
||||||
10
adolf/vw-mcp/Dockerfile
Normal file
10
adolf/vw-mcp/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
FROM node:22-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm install -g @bitwarden/cli
|
||||||
|
COPY package.json ./
|
||||||
|
RUN npm install --production
|
||||||
|
COPY vaultwarden.js server.js ./
|
||||||
|
COPY start.sh ./
|
||||||
|
RUN chmod +x start.sh
|
||||||
|
CMD ["./start.sh"]
|
||||||
27
adolf/vw-mcp/docker-compose.yml
Normal file
27
adolf/vw-mcp/docker-compose.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: vw-mcp-adolf
|
||||||
|
services:
|
||||||
|
vw-mcp-adolf:
|
||||||
|
build: .
|
||||||
|
container_name: vw-mcp-adolf
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- BITWARDENCLI_APPDATA_DIR=/bw-data
|
||||||
|
- NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||||
|
- HTTPS_PROXY=
|
||||||
|
- HTTP_PROXY=
|
||||||
|
- ALL_PROXY=
|
||||||
|
- https_proxy=
|
||||||
|
- http_proxy=
|
||||||
|
- all_proxy=
|
||||||
|
volumes:
|
||||||
|
# Dedicated, OWN volume — deliberately NOT the host bind mount
|
||||||
|
# (`/home/alvis/.config/Bitwarden CLI`) that agap-mcp/marketplace-mcp
|
||||||
|
# share, and NOT any other bw data dir. This bot's login/session state
|
||||||
|
# must never mix with the master account's or any other bot's.
|
||||||
|
- vw-mcp-adolf_bw-data:/bw-data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
vw-mcp-adolf_bw-data:
|
||||||
11
adolf/vw-mcp/package.json
Normal file
11
adolf/vw-mcp/package.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "vw-mcp-adolf",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"description": "Standalone, read-only MCP server giving Adolf a narrow slice of Vaultwarden (vw_get_password, vw_get_item, vw_list_items only), split out of agap-mcp per kb task #64",
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
|
"express": "^4.19.0",
|
||||||
|
"zod": "^3.23.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
137
adolf/vw-mcp/server.js
Normal file
137
adolf/vw-mcp/server.js
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
// vw-mcp-adolf — standalone, read-only Vaultwarden MCP server for Adolf (kb
|
||||||
|
// task #64).
|
||||||
|
//
|
||||||
|
// Gives Adolf a narrow, scoped slice of Vaultwarden WITHOUT exposing the
|
||||||
|
// master vault:
|
||||||
|
// - Only 3 read-only tools: vw_get_password, vw_get_item, vw_list_items.
|
||||||
|
// No write tools (vw_create_login / vw_update_password) exist here at
|
||||||
|
// all — omitted, not just unregistered, so there is no code path that
|
||||||
|
// could ever write to the vault.
|
||||||
|
// - Authenticates to Vaultwarden as a DEDICATED bot user
|
||||||
|
// (adolf-vault@auth.local), never the master account.
|
||||||
|
// - Server-side scoping is the real fence: that bot user is granted
|
||||||
|
// read-only access to a narrow "Adolf" collection only.
|
||||||
|
// - Every MCP transport requires `Authorization: Bearer $VW_MCP_TOKEN`,
|
||||||
|
// same pattern as marketplace-mcp (src/server.js) — refuses to start if
|
||||||
|
// VW_MCP_TOKEN is unset, so it can never silently run open. /health stays
|
||||||
|
// unauthenticated (no sensitive data, used for liveness checks).
|
||||||
|
import express from 'express';
|
||||||
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||||
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
||||||
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { initVaultwarden, vwGetPassword, vwGetItem, vwListItems, vwListOrgItems } from './vaultwarden.js';
|
||||||
|
|
||||||
|
const PORT = parseInt(process.env.PORT || '3105');
|
||||||
|
|
||||||
|
// --- Init ---
|
||||||
|
async function init() {
|
||||||
|
await initVaultwarden();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- MCP server factory (one per session — McpServer can't share transports) ---
|
||||||
|
function ok(text) {
|
||||||
|
return { content: [{ type: 'text', text: typeof text === 'string' ? text : JSON.stringify(text, null, 2) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function err(e) {
|
||||||
|
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createServer() {
|
||||||
|
const server = new McpServer({ name: 'vw-mcp-adolf', version: '1.0.0' });
|
||||||
|
|
||||||
|
server.tool('vw_get_password', 'Get password for a Vaultwarden item by name (read-only; scoped to the Adolf collection)', { name: z.string() },
|
||||||
|
async ({ name }) => {
|
||||||
|
try { return ok(vwGetPassword(name)); } catch (e) { return err(e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool('vw_get_item', 'Get full details of a Vaultwarden item (name, username, password, url, notes; read-only; scoped to the Adolf collection)', { name: z.string() },
|
||||||
|
async ({ name }) => {
|
||||||
|
try {
|
||||||
|
const item = vwGetItem(name);
|
||||||
|
return ok({ name: item.name, username: item.login?.username, password: item.login?.password, url: item.login?.uris?.[0]?.uri, notes: item.notes });
|
||||||
|
} catch (e) { return err(e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool('vw_list_items', 'List Vaultwarden items visible to the Adolf bot user (read-only). Searches its personal vault by default; set org=true to search the org (only the Adolf collection is actually visible)', {
|
||||||
|
search: z.string().optional(),
|
||||||
|
org: z.boolean().optional(),
|
||||||
|
}, async ({ search, org }) => {
|
||||||
|
try {
|
||||||
|
const items = org ? vwListOrgItems(search) : vwListItems(search);
|
||||||
|
return ok(items.map(i => ({ id: i.id, name: i.name, username: i.login?.username, url: i.login?.uris?.[0]?.uri })));
|
||||||
|
} catch (e) { return err(e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Auth gate ---
|
||||||
|
// This server holds real credentials (a narrow slice, but real), so every
|
||||||
|
// MCP transport requires a bearer token. VW_MCP_TOKEN lives in Vaultwarden
|
||||||
|
// (create it as its own item once the server is live) and is injected via
|
||||||
|
// docker-compose env — never hardcode it here. /health stays open (no
|
||||||
|
// sensitive data, used for liveness checks). If VW_MCP_TOKEN is unset the
|
||||||
|
// server refuses to start, so this can never silently run open.
|
||||||
|
const AUTH_TOKEN = process.env.VW_MCP_TOKEN;
|
||||||
|
if (!AUTH_TOKEN) {
|
||||||
|
console.error('VW_MCP_TOKEN env var is required (see docker-compose.yml / .env)');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireAuth(req, res, next) {
|
||||||
|
const header = req.get('authorization') || '';
|
||||||
|
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
|
||||||
|
if (token !== AUTH_TOKEN) {
|
||||||
|
return res.status(401).json({ jsonrpc: '2.0', error: { code: -32001, message: 'Unauthorized' }, id: null });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- HTTP server (Streamable HTTP + legacy SSE) ---
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
const sseTransports = new Map();
|
||||||
|
|
||||||
|
// Streamable HTTP — stateless: fresh server per request, survives container restarts
|
||||||
|
app.all('/mcp', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
||||||
|
res.on('close', () => transport.close());
|
||||||
|
await createServer().connect(transport);
|
||||||
|
await transport.handleRequest(req, res, req.body);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('MCP request error:', e.message);
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: e.message }, id: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Legacy SSE — kept for backward compatibility, same pattern as agap-mcp/kanboard-mcp
|
||||||
|
app.get('/sse', requireAuth, async (req, res) => {
|
||||||
|
const transport = new SSEServerTransport('/messages', res);
|
||||||
|
sseTransports.set(transport.sessionId, transport);
|
||||||
|
res.on('close', () => sseTransports.delete(transport.sessionId));
|
||||||
|
await createServer().connect(transport);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/messages', requireAuth, async (req, res) => {
|
||||||
|
const transport = sseTransports.get(req.query.sessionId);
|
||||||
|
if (!transport) return res.status(400).send('Unknown session');
|
||||||
|
await transport.handlePostMessage(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/health', (_, res) => res.json({ status: 'ok', tools: 3 }));
|
||||||
|
|
||||||
|
init()
|
||||||
|
.then(() => {
|
||||||
|
app.listen(PORT, () => console.log(`vw-mcp-adolf listening on :${PORT}`));
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.error('Init failed:', e.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
2
adolf/vw-mcp/start.sh
Executable file
2
adolf/vw-mcp/start.sh
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
exec node server.js
|
||||||
100
adolf/vw-mcp/vaultwarden.js
Normal file
100
adolf/vw-mcp/vaultwarden.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
// Trimmed, read-only copy of agap-mcp/src/vaultwarden.js (kb task #64).
|
||||||
|
//
|
||||||
|
// Deliberate differences from the master copy:
|
||||||
|
// - Only the 3 read tools are implemented: get password, get item, list
|
||||||
|
// items. vwCreateLogin / vwUpdatePassword (and AI_COLLECTION, which only
|
||||||
|
// those write paths needed) are NOT here — this server must never be able
|
||||||
|
// to write to the vault, even in principle.
|
||||||
|
// - Authenticates as a DEDICATED bot user (BW_EMAIL=adolf-vault@auth.local),
|
||||||
|
// never the master allogn@gmail.com account. No default email/password —
|
||||||
|
// both must be explicit in .env so this can never silently fall back to
|
||||||
|
// the master identity.
|
||||||
|
// - BITWARDENCLI_APPDATA_DIR (see docker-compose.yml) points at this
|
||||||
|
// service's OWN volume, separate from the agap-mcp/marketplace-mcp host
|
||||||
|
// bind mount (`/home/alvis/.config/Bitwarden CLI`) — the bot's bw
|
||||||
|
// login/session state must never share a directory with the master's.
|
||||||
|
//
|
||||||
|
// The real fence is server-side: the bot user is granted read-only access to
|
||||||
|
// a narrow "Adolf" collection only (not the whole "AI" collection). This
|
||||||
|
// client code does not filter by collection — Vaultwarden itself only
|
||||||
|
// returns items the bot user's permissions allow, whatever org-wide ORG_ID
|
||||||
|
// is passed.
|
||||||
|
import { execFileSync } from 'child_process';
|
||||||
|
|
||||||
|
const BW = 'bw';
|
||||||
|
const ORG_ID = '4bd75130-b4d3-48d4-a4cb-e52b70295a51';
|
||||||
|
|
||||||
|
let _session = null;
|
||||||
|
|
||||||
|
function bwEnv() {
|
||||||
|
const env = { ...process.env };
|
||||||
|
for (const k of ['HTTPS_PROXY', 'HTTP_PROXY', 'ALL_PROXY', 'https_proxy', 'http_proxy', 'all_proxy'])
|
||||||
|
delete env[k];
|
||||||
|
env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(args, input) {
|
||||||
|
return execFileSync(BW, args, {
|
||||||
|
env: bwEnv(),
|
||||||
|
encoding: 'utf8',
|
||||||
|
input,
|
||||||
|
stdio: input ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
|
||||||
|
}).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initVaultwarden() {
|
||||||
|
const email = process.env.BW_EMAIL;
|
||||||
|
const password = process.env.BW_PASSWORD;
|
||||||
|
const server = process.env.VW_URL;
|
||||||
|
if (!email || !password) {
|
||||||
|
throw new Error('BW_EMAIL and BW_PASSWORD env vars are required — the dedicated adolf-vault@auth.local bot credentials, never the master account (see .env.example)');
|
||||||
|
}
|
||||||
|
if (!server) {
|
||||||
|
throw new Error('VW_URL env var is required — this service owns a FRESH BITWARDENCLI_APPDATA_DIR volume (unlike agap-mcp/marketplace-mcp, which reuse the host dir that already has `bw config server` set), so it must configure the server itself on every boot (see .env.example)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idempotent — safe to call on every boot, including against an
|
||||||
|
// already-configured appdata dir.
|
||||||
|
run(['config', 'server', server]);
|
||||||
|
|
||||||
|
let status = 'unauthenticated';
|
||||||
|
try {
|
||||||
|
status = JSON.parse(run(['status'])).status;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (status === 'unauthenticated') {
|
||||||
|
run(['login', email, password, '--raw']);
|
||||||
|
}
|
||||||
|
|
||||||
|
_session = run(['unlock', password, '--raw']);
|
||||||
|
run(['sync', '--session', _session]);
|
||||||
|
console.log('Vaultwarden: ready (adolf-vault bot identity)');
|
||||||
|
}
|
||||||
|
|
||||||
|
function session() {
|
||||||
|
if (!_session) throw new Error('Vaultwarden not initialized');
|
||||||
|
return _session;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function vwGetPassword(name) {
|
||||||
|
return run(['get', 'password', name, '--session', session()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function vwGetItem(name) {
|
||||||
|
return JSON.parse(run(['get', 'item', name, '--session', session()]));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function vwListItems(search) {
|
||||||
|
const args = ['list', 'items', '--session', session()];
|
||||||
|
if (search) args.push('--search', search);
|
||||||
|
return JSON.parse(run(args));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function vwListOrgItems(search) {
|
||||||
|
// Server-side collection permissions (not this code) decide what actually
|
||||||
|
// comes back — the bot user only sees the narrow "Adolf" collection.
|
||||||
|
const args = ['list', 'items', '--organizationid', ORG_ID, '--session', session()];
|
||||||
|
if (search) args.push('--search', search);
|
||||||
|
return JSON.parse(run(args));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user