@try-works/dsh-righthand 0.1.8 → 0.1.10

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/README.md CHANGED
@@ -28,6 +28,7 @@ dsh --profile <profile> --patch ./cordis.patch.yml
28
28
  | `rh_task_create` / `rh_task_list` / `rh_task_next` / `rh_task_update` / `rh_task_delete` | tasks | `ctx.storageDomain` (typed `righthand_tasks` domain; state machine open → done/failed) |
29
29
  | `rh_text_summarise` / `rh_text_extract` / `rh_text_classify` / `rh_text_translate` | text | `ctx.llm` (one model call per verb) |
30
30
  | `rh_weather_forecast` / `rh_weather_air` | weather | Open-Meteo (keyless), every request through the SSRF-guarded fetcher |
31
+ | `rh_places_geocode` / `rh_places_address` / `rh_places_elevation` / `rh_places_nearby` | places | Nominatim/OSM + Open-Meteo (keyless; User-Agent per Nominatim's 1 req/s usage policy) |
31
32
  | `rh_files_put` / `rh_files_get` / `rh_files_list` / `rh_files_share` / `rh_files_delete` | files | Cloudflare R2 (S3-compatible, SigV4-signed; credentials from `R2_ACCESS_KEY_ID` / `R2_SECRET_ACCESS_KEY`, bucket from settings) |
32
33
  | `rh_events_create` / `rh_events_due` / `rh_events_list` / `rh_events_free` / `rh_events_cancel` | events | `ctx.storageDomain` (typed `righthand_events` domain; the agent is the scheduler — `rh_events_due` runs each turn) |
33
34
  | `rh_notify_send` | notify | ntfy.sh (keyless — the topic name is the only secret) |
@@ -74,7 +75,10 @@ Namespace "righthand" (registered by the secrets family):
74
75
  | `defaultNotifyTopic` | "" | default ntfy topic for `rh_notify_send` |
75
76
 
76
77
  `rh_settings_get` returns the resolved values; `rh_settings_set` merges a
77
- partial patch (persisted by the harness settings provider).
78
+ partial patch (persisted by the harness settings provider). Schema wall:
79
+ updates with keys the schema does not define are accepted but never come
80
+ back from `rh_settings_get` — register new keys in the plugin schema, or
81
+ keep ad-hoc knobs in `rh_store`.
78
82
 
79
83
  ## Blueprints
80
84
 
@@ -88,7 +92,7 @@ canonical example of the current DSH-native form (tool-to-tool via
88
92
  | Blueprint | What it builds |
89
93
  |---|---|
90
94
  | `blueprint/daily-digest` | Collect sources, summarize, send on a schedule — extractive locally, LLM synthesis as escalation |
91
- | `blueprint/data-adapter` | Wrap one keyless public API as a tool family: guarded fetch, normalize, one tool per query. Worked example ships in the plugin: `rh_weather_*` |
95
+ | `blueprint/data-adapter` | Wrap one keyless public API as a tool family: guarded fetch, normalize, one tool per query. Worked examples ship in the plugin: `rh_weather_*`, `rh_places_*` |
92
96
  | (phase 5) `rh_files_*` on R2 | The first family on an actual Cloudflare primitive: S3-compatible API, SigV4 signing pinned to the AWS test vector, credentials from the credential provider |
93
97
  | `blueprint/research-radar` | Multi-source research over a rolling window (Reddit, X, YouTube, HN, Polymarket, web): score, dedupe, synthesize with citations |
94
98
  | `blueprint/web-scraper` | Fetch a page, extract title/headings/links/text, make it searchable — static HTML first, render/vision as escalation |
@@ -107,6 +111,12 @@ families worth adding, from the tools catalogue + permission commit) and
107
111
  from the whole repo — single-source specs, golden permissions, layering,
108
112
  held-state gates, SSRF guarding).
109
113
 
114
+ [`docs/scenario-patterns.md`](docs/scenario-patterns.md) is the guidance
115
+ built by actually running the tools: key-prefix conventions, the receipt
116
+ pattern, undo trails, the settings schema wall, credential auth gates, and
117
+ exec collect-vs-background — each pattern live-verified against the real
118
+ harness services.
119
+
110
120
  ## Packaged skill
111
121
 
112
122
  The plugin ships the `dsh-righthand` skill
@@ -150,8 +160,9 @@ so the agent cannot deploy or destroy without the gate.
150
160
  The store, task and events families need `ctx.storageDomain`, which the web
151
161
  profile provides (`storage` + `storage-json` + `storage-domain` rows). In a
152
162
  profile without it — e.g. the headless bundle — those fourteen tools stay
153
- dormant and the other nineteen tools (secrets, settings, exec, text, weather,
154
- files, notify) still register; the plugin never fails the boot.
163
+ dormant and the other twenty-three tools (secrets, settings, exec, text,
164
+ weather, places, files, notify) still register; the plugin never fails the
165
+ boot.
155
166
 
156
167
  ## Development
157
168
 
@@ -0,0 +1,188 @@
1
+ # Agent tool catalog — analysis of micro.mu/tools + the permission commit
2
+
3
+ Source material:
4
+ - https://micro.mu/tools — the Mu tool catalogue (~30 services, ~130 methods,
5
+ every method an agent-callable tool).
6
+ - https://github.com/micro/mu/commit/4933862770d706f6c56e0ab2af287070811fc52d —
7
+ "five permission flags become two": the permission-model commit.
8
+
9
+ ## What the commit teaches (apply to guard-tools)
10
+
11
+ The commit collapses five declared flags into two derived facts:
12
+
13
+ | Mu model | Meaning |
14
+ |---|---|
15
+ | `Requires: Open` | anybody, including nobody at all |
16
+ | `Requires: Caller` | somebody — a funded wallet counts; enough for anything scoped to "theirs" (own notes, own files) |
17
+ | `Requires: Account` | a real account, not just a wallet — requests made *on somebody's behalf* (fetch any URL you name) or spending a shared reputation (mail.Send); "rationing needs somebody to ration" |
18
+ | `Destructive` | an irreversible effect nobody asked for; the flag exists because tool *descriptions* are attacker-controlled text, so a tool that holds one is a prompt-injection surface |
19
+
20
+ Lessons for righthand's guard:
21
+ 1. Mu's identity tiers (Open/Caller/Account) and its `limit` per-day counts
22
+ are **not applicable** here: righthand is a single user — the tools are
23
+ built by that user's own agent and run locally or on the user's own
24
+ Cloudflare account. There are no accounts to tier and nothing to ration.
25
+ What *does* carry over is the **destructive marker**: an irreversible
26
+ effect the agent's own tool descriptions could talk it into (a deploy, a
27
+ delete, a DNS change) is the prompt-injection surface the guard exists
28
+ for, and prefix-based allow/deny/ask already covers it — the marker just
29
+ makes the reasoning explicit.
30
+ 2. **Golden-file the derived permissions** (`test/permissions.golden`):
31
+ baseline every tool's permission from the derived policy and let the test
32
+ fail when one changes. The commit's own debugging is the guidance: the
33
+ golden first recorded *nothing* (registering a service is not deriving its
34
+ tools), then recorded only the derived policy (and missed the exact
35
+ distinction being collapsed), then recorded prices (failed on test order).
36
+ It now records permissions alone, sees the change, and is stable.
37
+ Righthand equivalent: a `tests/permissions.golden` over every `rh_*` tool.
38
+
39
+ ## Tool families Mu ships, mapped onto righthand primitives
40
+
41
+ Legend: **have** = the harness/righthand already provides this;
42
+ **add** = a new righthand family worth building; **data** = a keyless-API
43
+ adapter (blueprint material); **skip** = instance-local social or
44
+ third-party-keyed, out of scope for the core plugin.
45
+
46
+ ### Already provided (no work — document as guidance, don't rebuild)
47
+
48
+ | Mu | Righthand/harness equivalent |
49
+ |---|---|
50
+ | shell_run / shell_write | `rh_run` / `rh_run_bg` + `ctx.fs` write tools |
51
+ | web_fetch / web_search / browser_read / browser_shot | harness `web_search` + browser tools + `blueprint/web-scraper` |
52
+ | notes_add/get/list/delete | `rh_store_*` (the notes pattern, scenario 2) |
53
+ | tasks_create/list/update/next | `rh_store_*` task board (documented use case) — candidate for a typed `rh_task_*` family |
54
+ | recall_list/search/conversation | harness session services (`session-query-sqlite`, `session` projection) |
55
+ | docs_write/read/list | `ctx.fs` read/write/edit tools (store keyed index is the delta) |
56
+
57
+ ### New righthand families worth adding (core plugin)
58
+
59
+ | Family | Mu analogue | Backing | Effort |
60
+ |---|---|---|---|
61
+ | `rh_task_*` | tasks_* | `rh_store` domain `tasks` (state machine: open → done, `tasks_next`) | small — the store family already proves the pattern |
62
+ | `rh_text_*` | text_classify / text_extract / text_summarise / text_translate | `ctx.llm` (same route as digest summaries) | small — JSON-constrained prompts, one tool per verb |
63
+ | `rh_files_*` | files_put/get/list/share | Cloudflare R2 (put → object URL; share → public/private toggle) | medium — first real "Cloudflare primitive" family |
64
+ | `rh_notify_*` | notify_send/devices | ntfy.sh topic (keyless — the topic name is the only secret, no token) or web-push (self-generated VAPID); store device/topic registry in `rh_store` | small |
65
+ | `rh_events_*` | events_create/list/free | `rh_store` + Cloudflare cron trigger (or OS scheduler locally) | medium — scheduling is the hard half |
66
+
67
+ ### Keyless data adapters (blueprint material — one pattern, many tools)
68
+
69
+ > Constraint (2026-08-30): third-party services are allowed **only when they
70
+ > need no API token or credentials** — keyless public endpoints. Anything
71
+ > that needs a key (Twilio, Resend, paid tiers) is out. All of these are
72
+ > keyless.
73
+
74
+ These are all the same shape as `blueprint/daily-digest` + `research-radar`
75
+ adapters: a keyless HTTP API, a probe + fixture test, normalize, cache in
76
+ `rh_store`, optional LLM summarization. One `blueprint/data-adapter` recipe
77
+ would generate the whole set.
78
+
79
+ | Mu | Keyless source |
80
+ |---|---|
81
+ | weather_air / weather_forecast / weather_history / weather_marine | Open-Meteo (no key) |
82
+ | places_geocode / places_address / places_elevation / places_nearby / places_search | Nominatim (OSM), no key — **shipped 0.1.9** as `rh_places_geocode` / `rh_places_address` / `rh_places_elevation` / `rh_places_nearby` (search is covered by geocode) |
83
+ | flights_airport / flights_overhead / flights_track | OpenSky Network (anonymous tier) |
84
+ | hazards_quakes / hazards_alerts / hazards_floods | USGS / GDACS / UK EA flood API |
85
+ | food_product / food_search | Open Food Facts (no key) |
86
+ | markets_convert / markets_list | CoinGecko / exchangerate APIs (free tier) |
87
+ | routes_directions / routes_eta / routes_nearest | OSRM public server |
88
+ | maps_tile / maps_area | static tile URLs (OSM) |
89
+ | news_list / news_read / news_search | RSS + index (the daily-digest fetcher generalised) |
90
+
91
+ ### Out of scope for the core plugin (note in guidance, don't build)
92
+
93
+ | Mu | Why skip |
94
+ |---|---|
95
+ | prayer_* (times, qibla, verse, hadith, reflection, search) | Muslim/Islamic tools are out of scope by explicit instruction |
96
+ | mail_send / mail_inbox | needs a mail provider key (Resend/MailChannels) — violates the keyless rule; account-scoped reputation |
97
+ | sms_send / sms_history | Twilio-style key — violates the keyless rule |
98
+ | images_generate | Workers AI image gen is the natural fit — the user's own Cloudflare account, optional paid plugin |
99
+ | wallet_* (USDC on Base) | custodial payments, regulatory surface |
100
+ | chat/social/stream/blog/users | instance-local social graph — nothing to attach to in a personal harness |
101
+ | apps_create/build/fork | a whole hosting surface; closest righthand form is Cloudflare Workers deployment (`blueprint/vision-worker` shape), a phase-2 plugin |
102
+ | transit_* (TfL) | UK-only, keyed in parts; a data adapter if a user asks |
103
+
104
+ ## Suggested build order
105
+
106
+ > Constraint (2026-08-30): TypeScript only, no Go. Single user — no accounts,
107
+ > no rationing, no identity tiers. Every tool is built by the user's own
108
+ > agent and runs locally or on the user's own Cloudflare account. The
109
+ > `requires`/`limit` axes from Mu are dropped; the `destructive` marker and
110
+ > the golden-file discipline stay.
111
+
112
+ 1. **Guard upgrade**: `destructive: true` marker on guard rules + a
113
+ `tests/permissions.golden` over all `rh_*` tools recording destructive
114
+ flags and guard modes. Prefix-based allow/deny/ask stays — the marker
115
+ documents *why* a prefix is guarded, and the golden catches drift.
116
+ 2. **`rh_task_*` + `rh_text_*`** — both tiny over existing services, and
117
+ together they cover Mu's most-used families (tasks, notes, text).
118
+ 3. **One data-adapter blueprint** (weather or places as the demo) + the
119
+ recipe that generates the rest, all running locally or as the user's own
120
+ Workers.
121
+ 4. **`rh_files_*` on R2** — the first family that is *actually on a
122
+ Cloudflare primitive* (the user's own R2), the plugin's stated purpose.
123
+ 5. `rh_events_*`, `rh_notify_*`, then the out-of-scope list as optional
124
+ plugins.
125
+
126
+ ## Incorporation decisions (from mu-repo-analysis.md)
127
+
128
+ > Constraints (2026-08-30): **TypeScript only — no Go.** Single user —
129
+ > **no accounts, no rationing**: nothing to tier, nothing to meter per-day.
130
+ > Every tool is built by the user's own agent and runs locally or on the
131
+ > user's own Cloudflare account. Mu lessons that presuppose a multi-user
132
+ > server (identity tiers, limits, quotas, wallets, address spaces) are out;
133
+ > the engineering discipline carries over.
134
+
135
+ The architecture read produced eleven lessons; these are the ones worth
136
+ incorporating now, each mapped to a concrete righthand change, in order.
137
+ Deliberately NOT incorporated below the fold.
138
+
139
+ ### Incorporate now
140
+
141
+ 1. **Permissions golden first** (Mu lesson 2). Record the answer before
142
+ changing the model: `tests/permissions.golden`, one line per `rh_*` tool
143
+ recording its **destructive flag** and guard mode. Any guard refactor
144
+ that leaves the file unchanged is behaviour-preserving; any diff is the
145
+ list of doors that moved.
146
+ 2. **Guard rules gain a `destructive` marker** (lessons 2). Prefix-based
147
+ allow/deny/ask stays; `destructive: true` documents *why* a prefix is
148
+ guarded — irreversible effects (deploy, delete, DNS change) are the
149
+ prompt-injection surface. The marker is declarative only: the enforcement
150
+ is still the rule's mode. No identity tiers, no limits — there is one
151
+ user and nothing to ration.
152
+ 3. **Scope = catalogue** (lesson 10). The visible catalogue and the
153
+ enforceable scope must be one mechanism: the SKILL.md and README tool
154
+ tables must state what a guard rule gates, and the golden must derive
155
+ from the same declaration — no second hand-kept list.
156
+ 4. **`rh_task_*` family** (lesson 4). Tasks domain over the store:
157
+ create/list/next/update/delete with a state machine; descriptions carry
158
+ the "always say something" rule — a failed run is delivered like an
159
+ answer.
160
+ 5. **`rh_text_*` family** (lesson 7). summarise/extract/classify/translate
161
+ over `ctx.llm`, reusing the digest summarizer's prompt discipline: roles
162
+ stay roles, caps are deliberate, per-call failure contained.
163
+ 6. **Settings hygiene** (lesson 11). Every settings key must have a
164
+ consumer or go. The namespace holds the user's own Cloudflare account
165
+ facts (`accountId`, `defaultZone`, `defaultScriptPrefix`) — no pricing,
166
+ no limits.
167
+ 7. **Data-adapter blueprint with the SSRF checklist** (lesson 6). The
168
+ fetcher blueprint guidance must carry: block non-public destinations,
169
+ revalidate every redirect hop, cap size and time. Adapters may call any
170
+ **keyless** public endpoint — an API token requirement is the line that
171
+ puts a service out of scope.
172
+
173
+ ### Deliberately not incorporated
174
+
175
+ | Mu feature | Reason |
176
+ |---|---|
177
+ | Go services / handlers | righthand is TypeScript on the DSH harness |
178
+ | `requires` identity tiers (Open/Caller/Account) | single user — no accounts to tier |
179
+ | `limit` per-day counts / quota.json pricing | single user — nothing to ration, nothing to meter |
180
+ | Spec.Card renderers | the web GUI owns turn summaries; righthand outputs carry `fetchedAt` instead |
181
+ | held-state judge (agent/gate) | guard `ask` is the local judge; a full inbound queue needs a product surface |
182
+ | agents-as-accounts / inbox address space | a product, not a plugin — the DSH harness already is the agent host |
183
+ | wallet_* (USDC, x402) | payments/regulatory surface, absent by design |
184
+ | memory-as-history fix | already how the DSH agent loop works — only prompt-building tools inherit the rule |
185
+ | internal/ layering test | Cordis + the harness enforce the equivalent; the tool-to-tool rule goes in guidance instead |
186
+ | keyed third parties (Twilio, Resend, paid APIs) | the keyless rule: any service that requires an API token or credentials is out |
187
+ | prayer_* / Muslim or Islam-related tools | out of scope by explicit instruction |
188
+
@@ -0,0 +1,194 @@
1
+ # Mu repository analysis — architecture lessons for righthand
2
+
3
+ Source: https://github.com/micro/mu (Go, ~1200 go files). This is the
4
+ *architecture* read of the repo, not the tool list — that is in
5
+ [agent-tool-catalog.md](agent-tool-catalog.md). Each section names the Mu
6
+ mechanism, the failure it fixed, and the righthand/DSH equivalent to adopt
7
+ or adapt.
8
+
9
+ ## 1. One declaration, every surface derives (internal/service/spec.go)
10
+
11
+ Every service declares itself once — name, description, page, icon,
12
+ endpoints with model-facing docs, cost, permissions, and a Card renderer —
13
+ and every surface *derives* from that Spec: MCP tool list, REST reference,
14
+ nav, guest allowlists, agent labels, the destructive list, the account-
15
+ scoped list, pricing ops, and two documentation tables. Before the Spec
16
+ there were **fourteen hand-written lists**, each correct alone and none
17
+ aware of the others — the same capability was called search, search_web,
18
+ index and web_search at once, and Stream and Chat shared an icon for months
19
+ because nothing could notice the repeat.
20
+
21
+ **Righthand equivalent:** the `rh_*` tool family. Today each tool declares
22
+ its description/params/output inline in `defineTool`. The Mu lesson is to
23
+ declare the *spec* once and derive the catalogue — in DSH the tool registry
24
+ already derives the model-facing list; what righthand should add is the
25
+ derived **permissions golden** (§2) and a derived **docs table** (README,
26
+ skill, catalog) rather than hand-kept lists.
27
+
28
+ ## 2. Golden-file the derived permissions (test/permissions.golden)
29
+
30
+ `permissions.golden` records one line per tool — needsAccount, destructive,
31
+ bound/open, accountOnly, optionalAuth — and the test fails when any derived
32
+ permission moves. The file's own debugging is the guidance: the first
33
+ version recorded **nothing** (registering a service is not deriving its
34
+ tools — an empty golden that passed, worse than none because it looks like
35
+ cover); the second recorded only the derived policy and missed the exact
36
+ distinction being collapsed; the third recorded prices and failed on test
37
+ order. It now records permissions alone, sees the change, and is stable.
38
+
39
+ **Righthand equivalent:** `tests/permissions.golden` over every `rh_*` tool
40
+ (and, once they exist, the rt_* test tools): one line per tool with the
41
+ guard-relevant facts. Refactors that are behaviour-preserving leave the file
42
+ untouched; any diff is the list of doors that moved.
43
+
44
+ ## 3. Layer discipline, asserted by test (test/layering_test.go)
45
+
46
+ Product packages (home, agent, service, admin, account) may import
47
+ internal/; internal/ may never import the product, with two exceptions —
48
+ the assembly programs. The rule is enforced by a regex test, and the bill
49
+ for every past violation is legible: internal/server/hooks.go, seven hundred
50
+ lines of function variables whose only job was to hand one package a pointer
51
+ to another because they could not import each other. Also: a service may
52
+ not import the wallet — asking what something costs is internal/quota, which
53
+ knows prices and not balances.
54
+
55
+ **Righthand equivalent:** the tool modules are already thin consumers over
56
+ harness services. The rule to write down: a tool module answers a question
57
+ about state; an *agent* (or a digest-type helper) decides which question to
58
+ ask — services announce events, agents subscribe (Mu's agent/gate,
59
+ agent/work, agent/digest all work this way). In DSH terms that is
60
+ `ctx.on(...)` + the session-watcher blueprint, never a tool that imports
61
+ another tool's internals — call it by name through `ctx.tools.execute`.
62
+
63
+ ## 4. Agents are accounts; work is assigned, not invoked
64
+
65
+ An agent is a user in the same address space — it holds an address
66
+ (mail/XMPP) and can be written to. Tasks assign work (`service/tasks` holds
67
+ it, `agent/work` runs it); a standing instruction falling due and a task
68
+ assigned are the same fact. Results always say something: a run that could
69
+ not happen is delivered like an answer, because "a standing instruction
70
+ that goes quiet looks like the instruction was forgotten."
71
+
72
+ **Righthand equivalent:** `rh_task_*` (already the top build item in the
73
+ catalog doc) + `blueprint/session-watcher` for standing instructions. The
74
+ "always say something" rule belongs in the tool descriptions: a failed
75
+ scheduled job must produce a visible record, not silence.
76
+
77
+ ## 5. The held state — fail visible, release when in doubt (agent/gate, internal/thread)
78
+
79
+ A conversation is in the record or it is not; the binary is what made
80
+ unsolicited inbound impossible to handle (dropping is undetectable, filing
81
+ wakes a paid agent). So: **held** — in the record, visible, searchable, and
82
+ nothing acts on it until a judge (one model call per arrival) releases it.
83
+ The judge is *reluctant on purpose*: the costly mistake is holding a real
84
+ message (the plumber texting he is outside), so the prompt releases when in
85
+ doubt. Fail-safe direction: if the model is down, arrivals sit held and
86
+ visible — never silently dropped.
87
+
88
+ **Righthand equivalent:** guard `ask` mode is the local judge; the posture
89
+ to copy is: deny loudly and visibly, never silently drop, and when in doubt
90
+ let it through. Also relevant to `blueprint/inbound-webhook-pipeline`'s
91
+ idempotency dedupe.
92
+
93
+ ## 6. Defensive web: safefetch (SSRF guard) + safety (generation policy)
94
+
95
+ `internal/safefetch`: an SSRF-guarded HTTP client for fetching untrusted
96
+ URLs — refuses loopback, private ranges, link-local (including the
97
+ 169.254.169.254 metadata address), and multicast; validates **every
98
+ redirect hop**, not just the first; caps response size (2 MiB) and time
99
+ (10 s). `internal/safety`: what the instance will not *generate* — one
100
+ category refused always and not configurable, one category refused by
101
+ default and an operator's decision; a door, not a classifier ("it stops
102
+ casual misuse and raises the cost of the rest").
103
+
104
+ **Righthand equivalent:** the harness's web tools already own SSRF
105
+ protection; the blueprint guidance for any righthand fetcher should carry
106
+ the checklist (block non-public destinations, revalidate redirects, cap
107
+ size/time) and the generation-policy split belongs in the guard/skill
108
+ guidance, not per-tool ad hoc.
109
+
110
+ ## 7. Memory is real message history (agent/memory.go)
111
+
112
+ The conversation went to the model as prose inside one user message, with
113
+ assistant turns truncated to 300 chars and the whole blob sent twice. The
114
+ model never saw its own turns as its own; truncation made "what was the
115
+ third thing you listed?" unanswerable by construction. The fix: turns in
116
+ their real roles, nothing truncated, nothing duplicated.
117
+
118
+ **Righthand equivalent:** already the harness model (the DSH agent loop
119
+ does exactly this). For tools that *build prompts* — the digest summarizer,
120
+ any rh_text_* tool — the rule is the same: assistant content stays assistant
121
+ content, and input caps must be deliberate, not string-worries.
122
+
123
+ ## 8. Cards: a view, not a widget — and time is the missing field (Spec.Card)
124
+
125
+ Each service can render a Card; the field that mattered was `At` — a
126
+ headline from four minutes ago and a forecast are different kinds of thing,
127
+ so a card that knows when belongs in a chronology (stream) and one that
128
+ does not is a standing view. The Viewer is a struct ("who is looking" is
129
+ domain data, not context values), and the zero value is the shared render,
130
+ safe to cache; personal renders must never be cached. A card is a view — it
131
+ renders and links, holds no state and takes no input.
132
+
133
+ **Righthand equivalent:** the digest MDX already records `fetchedAt`; the
134
+ same discipline generalises — any rh_* summary output should say *when what
135
+ it shows happened* so a stream can order it honestly.
136
+
137
+ ## 9. Pricing: price vs limit, and an operator file (quota.json)
138
+
139
+ An operation is charged when it costs the instance something (a model call
140
+ or third-party request); storage and talking-to-your-agent are 0 because
141
+ they have no marginal cost — charging taxed exactly the behaviour the
142
+ product wants more of. `limit` is a hard per-day count, checked *before*
143
+ price, for the operations that reach a stranger — a price stops somebody
144
+ who has to pay and does nothing about a loop. quota.json is an operator's
145
+ file, not code.
146
+
147
+ **Righthand equivalent:** the settings namespace is the quota.json slot;
148
+ the split belongs in guard rules (a rate/limit axis beside allow/deny/ask).
149
+
150
+ ## 10. Tool scoping: the list follows the token (internal/api/listscope)
151
+
152
+ Scoping was enforced at dispatch and ignored by tools/list, so a confined
153
+ agent was handed every tool definition and found out by trial and error
154
+ that almost all were refused — the context cost of the whole catalogue and
155
+ none of its use. Fixed: the listing follows the token's scope; an explicit
156
+ `?tools=` list can only narrow further.
157
+
158
+ **Righthand equivalent:** guard rules are the scope; the lesson is that the
159
+ *visible* catalogue and the *enforceable* scope must be one mechanism, or
160
+ the agent pays context for tools it cannot use.
161
+
162
+ ## 11. Delete the dead switch (one model tier; five flags → two)
163
+
164
+ Two recurring refactors: the Fast/Best model tier outlived its only control
165
+ (nothing sent the field) and was deleted — "a premium tier needs a control,
166
+ a price somebody has decided on, and something on the page saying what the
167
+ difference buys... not a switch to leave lying around"; and the permission
168
+ flags collapsed from five to two because three of them were three ways of
169
+ saying the same thing and one was dead code nobody had ever set.
170
+
171
+ **Righthand equivalent:** every config key in the righthand settings
172
+ namespace must have a consumer or go; every guard rule field must have a
173
+ meaning a test can see (the golden file is what makes the difference
174
+ visible).
175
+
176
+ ## What this means for the righthand build order
177
+
178
+ > Constraints (2026-08-30): TypeScript only — no Go. Single user — no
179
+ > accounts, no rationing: Mu's identity tiers (`requires`) and per-day
180
+ > `limit` counts are out; the `destructive` marker and the golden-file
181
+ > discipline stay. Tools are built by the user's own agent and run locally
182
+ > or on the user's own Cloudflare account.
183
+
184
+ 1. **Spec + golden first**: a permissions.golden over the existing `rh_*`
185
+ tools, then the `destructive` marker it records. This is Mu's own
186
+ ordering — record the answer before changing the model.
187
+ 2. **`rh_task_*` + `rh_text_*`** (catalog doc §build order) — tasks
188
+ implements §4 (assigned work, always say something), text implements
189
+ §7's prompt discipline.
190
+ 3. **Data adapters with the safefetch checklist** in the blueprint guidance
191
+ — §6.
192
+ 4. **Scope = catalogue**: when guard scoping lands, the skill/catalog text
193
+ must reflect the same rule — §10.
194
+
@@ -0,0 +1,107 @@
1
+ # Scenario patterns - righthand recipes, tested by building
2
+
3
+ > Every recipe here ran live on 2026-08-31 against the real harness
4
+ > services (store, settings, credentials, subprocess, jobs) through the
5
+ > agent-facing `rh_*` tools, then the state was cleaned back up. Evidence
6
+ > of each run is inline; re-run them any time - they are safe.
7
+
8
+ ## The key conventions (one store, many collections)
9
+
10
+ `rh_store_*` is one key space. The prefix IS the collection;
11
+ `rh_store_list` + a prefix filter is the query language. Conventions:
12
+
13
+ | Prefix | Collection | Example value shape |
14
+ |---|---|---|
15
+ | `task:` | task board | `{ state: 'open', result? }` |
16
+ | `digest:` | daily digests | `{ top: [...], fetchedAt }` |
17
+ | `run:` / `uptime:` / `expense:` | dated logs | one record per date, aggregate by scan |
18
+ | `deploy:` / `dns:` / `tidy:` / `smoke:` | receipts and undo trails | `{ before, after, exitCode, at }` |
19
+ | anything else | ad-hoc | JSON-serializable only |
20
+
21
+ Absence is `{ found: false }`, not an error - that is the exists() check.
22
+ Every `rh_store_put` returns a global `writes` counter; the increment
23
+ sequence is the audit trail.
24
+
25
+ ## The receipt pattern (scenario 4, live-verified)
26
+
27
+ Build with `rh_run`, keep the result as one receipt key:
28
+
29
+ ```
30
+ rh_run { argv: ['node', 'scripts/fetch-digest.mjs', 'tests/fixtures/day2.json', '5'], cwd: 'D:/righthand-test' }
31
+ // -> exit 0, stdout = the fetcher's JSON (parse it in the caller)
32
+ rh_store_put { key: 'digest:scenario-test:2026-08-31',
33
+ value: { source: 'day2.json', count: 2, fresh: ['4'],
34
+ top: [{ id: '4', title: '...' }], fetchedAt: '<iso>' } }
35
+ rh_store_get { key: 'digest:scenario-test:2026-08-31' } // found: true, full value back
36
+ rh_store_delete { key: 'digest:scenario-test:2026-08-31' } // clean up when done
37
+ ```
38
+
39
+ The diff step that makes a digest a digest: fetch today and yesterday,
40
+ compare id sets in the caller, store only `fresh` in the receipt.
41
+
42
+ ## Undo trails (scenario 7 pattern)
43
+
44
+ Any mutating build writes what it is about to change BEFORE doing it,
45
+ then the after-state:
46
+
47
+ ```
48
+ rh_store_put { key: 'tidy:<ts>', value: { moved: [{ from, to }], at: '<iso>' } }
49
+ // ... do the moves with rh_run ...
50
+ // revert = read tidy:<ts> and move each { to -> from } in reverse
51
+ ```
52
+
53
+ ## Settings: the schema wall (live finding)
54
+
55
+ `rh_settings_set` accepts any patch - including keys the schema does not
56
+ define: `{ weeklyBudget: 200 }` returned `applied: true`. But
57
+ `rh_settings_get` only ever returns schema-known keys, so the custom key
58
+ never comes back. A scenario that needs its own config knob has exactly
59
+ two honest options:
60
+
61
+ 1. register the key in the righthand settings schema (a plugin change), or
62
+ 2. keep the knob in `rh_store` under its own prefix.
63
+
64
+ Never depend on an unregistered settings key - it looks written and
65
+ silently disappears.
66
+
67
+ ## Credentials: the auth gate (scenario 1/2, live-verified)
68
+
69
+ `rh_credential_describe` is the first step of any Cloudflare scenario and
70
+ it answers yes/no without leaking the value:
71
+
72
+ ```
73
+ rh_credential_describe { ref: 'CLOUDFLARE_API_TOKEN' }
74
+ // { configured: false, writable: true } -> stop, tell the user what to set
75
+ ```
76
+
77
+ `set` stores, `unset` removes, and no output ever contains the secret
78
+ (live-verified with a throwaway ref: set -> describe configured:true ->
79
+ unset -> configured:false; the value never appeared).
80
+
81
+ ## Exec: collect vs background (live finding)
82
+
83
+ | | `rh_run` | `rh_run_bg` |
84
+ |---|---|---|
85
+ | Returns | exit code + bounded stdout/stderr | a job id |
86
+ | Use when | you need the output in this turn | the command is long and this turn must go on |
87
+ | Polling | - | the job registry reports status/exit (`job_list` / `job_output`) |
88
+ | Output capture | yes (bounded) | NOT captured by the local registry - status and exit only |
89
+
90
+ Live-verified: `rh_run` curl returned HTTP headers + exit 0, and a failing
91
+ `node -e` returned exit 3 with stderr. `rh_run_bg` settled completed with
92
+ exit 0 and an empty output tail - background is for fire-and-forget and
93
+ status polling, collect mode is for answers.
94
+
95
+ ## Cleanup discipline
96
+
97
+ Scenarios write, verify, then delete their keys - the live runs above
98
+ left the store exactly as found (baseline keys only). If a run is a
99
+ deliverable rather than a test, the receipt key stays and IS the record.
100
+
101
+ ## Scenario matrix
102
+
103
+ `D:/righthand-test/SCENARIOS.md` holds the full tool-per-step matrix for
104
+ nine scenarios (deploy, DNS, grocery, digest, uptime, workout, downloads
105
+ tidy, budget, smoke test). The ones exercised live here: grocery (3),
106
+ digest (4), uptime (5), workout (6), plus the credential/settings/exec
107
+ cross-cuts shared by the rest.
package/lib/index.d.ts CHANGED
@@ -9,9 +9,9 @@
9
9
  * and the packaged skill as child fibers; each child waits on its own
10
10
  * services, so a
11
11
  * profile without `storageDomain` (e.g. headless) simply gets the
12
- * store/task/events families dormant and the other nineteen tools
13
- * (secrets, settings, exec, text, weather, files, notify) instead of
14
- * failing the boot.
12
+ * store/task/events families dormant and the other twenty-three tools
13
+ * (secrets, settings, exec, text, weather, places, files, notify) instead
14
+ * of failing the boot.
15
15
  *
16
16
  * Modules (each individually mountable via `export * as ...`):
17
17
  * - store-tools: rh_store_put/get/delete/list over ctx.storageDomain
@@ -19,6 +19,7 @@
19
19
  * - task-tools: rh_task_create/list/next/update/delete over ctx.storageDomain
20
20
  * - text-tools: rh_text_summarise/extract/classify/translate over ctx.llm
21
21
  * - weather-tools: rh_weather_forecast/air over Open-Meteo (keyless, SSRF-guarded)
22
+ * - places-tools: rh_places_geocode/address/elevation/nearby (Nominatim + Open-Meteo, keyless)
22
23
  * - files-tools: rh_files_put/get/list/share/delete over Cloudflare R2
23
24
  * - events-tools: rh_events_create/due/list/free/cancel over ctx.storageDomain
24
25
  * - notify-tools: rh_notify_send over ntfy.sh (keyless)
@@ -36,6 +37,7 @@ import * as guardTools from './guard-tools.ts';
36
37
  import * as taskTools from './task-tools.ts';
37
38
  import * as textTools from './text-tools.ts';
38
39
  import * as weatherTools from './weather-tools.ts';
40
+ import * as placesTools from './places-tools.ts';
39
41
  import * as filesTools from './files-tools.ts';
40
42
  import * as eventsTools from './events-tools.ts';
41
43
  import * as notifyTools from './notify-tools.ts';
@@ -56,16 +58,18 @@ export interface RighthandConfig {
56
58
  * instead of failing the whole plugin.
57
59
  */
58
60
  export declare function apply(ctx: Context, config?: RighthandConfig): void;
59
- export { storeTools, secretsTools, execTools, taskTools, textTools, weatherTools, filesTools, eventsTools, notifyTools, guardTools, righthandSkills };
61
+ export { storeTools, secretsTools, execTools, taskTools, textTools, weatherTools, placesTools, filesTools, eventsTools, notifyTools, guardTools, righthandSkills };
60
62
  export { SKILL_NAME, SKILL_DESCRIPTION, skillDirectory, skillBody } from './skills.ts';
61
63
  export type { GuardRule } from './guard-tools.ts';
62
64
  export { guardFactsFor } from './guard-tools.ts';
63
65
  export type { GuardFacts } from './guard-tools.ts';
64
66
  export { isBlockedIP, guardedFetch, normalizeForecast, normalizeAir } from './weather-tools.ts';
67
+ export { normalizePlace, normalizePlaces, normalizeElevation, distanceKm, nearbyViewbox } from './places-tools.ts';
65
68
  export { createR2Client, R2_ACCESS_KEY_REF, R2_SECRET_KEY_REF } from './files-tools.ts';
66
69
  export { signRequest, presignGet } from './sigv4.ts';
67
70
  export { freeSlots } from './events-tools.ts';
68
71
  export type { Event } from './events-tools.ts';
69
72
  export type { R2Options, ListEntry } from './files-tools.ts';
70
73
  export type { WeatherConfig, ForecastOut, AirOut } from './weather-tools.ts';
74
+ export type { PlacesConfig, PlaceOut, ElevationOut } from './places-tools.ts';
71
75
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,YAAY,MAAM,oBAAoB,CAAA;AAClD,OAAO,KAAK,SAAS,MAAM,iBAAiB,CAAA;AAC5C,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,SAAS,MAAM,iBAAiB,CAAA;AAC5C,OAAO,KAAK,SAAS,MAAM,iBAAiB,CAAA;AAC5C,OAAO,KAAK,YAAY,MAAM,oBAAoB,CAAA;AAClD,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,WAAW,MAAM,mBAAmB,CAAA;AAChD,OAAO,KAAK,WAAW,MAAM,mBAAmB,CAAA;AAChD,OAAO,KAAK,eAAe,MAAM,aAAa,CAAA;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAEjD,eAAO,MAAM,IAAI,kBAAkB,CAAA;AAEnC,iFAAiF;AACjF,eAAO,MAAM,MAAM,EAAE,MAAM,EAAO,CAAA;AAElC,qCAAqC;AACrC,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,KAAK,CAAC,EAAE,SAAS,EAAE,CAAA;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,eAAoB,GAAG,IAAI,CAYtE;AAGD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,eAAe,EAAE,CAAA;AACrJ,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACtF,YAAY,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAC/F,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACvF,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AACpD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAC7C,YAAY,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAC9C,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC5D,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,YAAY,MAAM,oBAAoB,CAAA;AAClD,OAAO,KAAK,SAAS,MAAM,iBAAiB,CAAA;AAC5C,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,SAAS,MAAM,iBAAiB,CAAA;AAC5C,OAAO,KAAK,SAAS,MAAM,iBAAiB,CAAA;AAC5C,OAAO,KAAK,YAAY,MAAM,oBAAoB,CAAA;AAClD,OAAO,KAAK,WAAW,MAAM,mBAAmB,CAAA;AAChD,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,WAAW,MAAM,mBAAmB,CAAA;AAChD,OAAO,KAAK,WAAW,MAAM,mBAAmB,CAAA;AAChD,OAAO,KAAK,eAAe,MAAM,aAAa,CAAA;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAEjD,eAAO,MAAM,IAAI,kBAAkB,CAAA;AAEnC,iFAAiF;AACjF,eAAO,MAAM,MAAM,EAAE,MAAM,EAAO,CAAA;AAElC,qCAAqC;AACrC,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,KAAK,CAAC,EAAE,SAAS,EAAE,CAAA;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,eAAoB,GAAG,IAAI,CAatE;AAGD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,eAAe,EAAE,CAAA;AAClK,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACtF,YAAY,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAC/F,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,kBAAkB,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAClH,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACvF,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AACpD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAC7C,YAAY,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAC9C,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC5D,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAC5E,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA"}
package/lib/index.js CHANGED
@@ -9,9 +9,9 @@
9
9
  * and the packaged skill as child fibers; each child waits on its own
10
10
  * services, so a
11
11
  * profile without `storageDomain` (e.g. headless) simply gets the
12
- * store/task/events families dormant and the other nineteen tools
13
- * (secrets, settings, exec, text, weather, files, notify) instead of
14
- * failing the boot.
12
+ * store/task/events families dormant and the other twenty-three tools
13
+ * (secrets, settings, exec, text, weather, places, files, notify) instead
14
+ * of failing the boot.
15
15
  *
16
16
  * Modules (each individually mountable via `export * as ...`):
17
17
  * - store-tools: rh_store_put/get/delete/list over ctx.storageDomain
@@ -19,6 +19,7 @@
19
19
  * - task-tools: rh_task_create/list/next/update/delete over ctx.storageDomain
20
20
  * - text-tools: rh_text_summarise/extract/classify/translate over ctx.llm
21
21
  * - weather-tools: rh_weather_forecast/air over Open-Meteo (keyless, SSRF-guarded)
22
+ * - places-tools: rh_places_geocode/address/elevation/nearby (Nominatim + Open-Meteo, keyless)
22
23
  * - files-tools: rh_files_put/get/list/share/delete over Cloudflare R2
23
24
  * - events-tools: rh_events_create/due/list/free/cancel over ctx.storageDomain
24
25
  * - notify-tools: rh_notify_send over ntfy.sh (keyless)
@@ -35,6 +36,7 @@ import * as guardTools from "./guard-tools.js";
35
36
  import * as taskTools from "./task-tools.js";
36
37
  import * as textTools from "./text-tools.js";
37
38
  import * as weatherTools from "./weather-tools.js";
39
+ import * as placesTools from "./places-tools.js";
38
40
  import * as filesTools from "./files-tools.js";
39
41
  import * as eventsTools from "./events-tools.js";
40
42
  import * as notifyTools from "./notify-tools.js";
@@ -55,6 +57,7 @@ export function apply(ctx, config = {}) {
55
57
  ctx.plugin(taskTools);
56
58
  ctx.plugin(textTools);
57
59
  ctx.plugin(weatherTools);
60
+ ctx.plugin(placesTools);
58
61
  ctx.plugin(filesTools);
59
62
  ctx.plugin(eventsTools);
60
63
  ctx.plugin(notifyTools);
@@ -62,10 +65,11 @@ export function apply(ctx, config = {}) {
62
65
  ctx.plugin(righthandSkills);
63
66
  }
64
67
  // Individual modules stay importable for selective mounting.
65
- export { storeTools, secretsTools, execTools, taskTools, textTools, weatherTools, filesTools, eventsTools, notifyTools, guardTools, righthandSkills };
68
+ export { storeTools, secretsTools, execTools, taskTools, textTools, weatherTools, placesTools, filesTools, eventsTools, notifyTools, guardTools, righthandSkills };
66
69
  export { SKILL_NAME, SKILL_DESCRIPTION, skillDirectory, skillBody } from "./skills.js";
67
70
  export { guardFactsFor } from "./guard-tools.js";
68
71
  export { isBlockedIP, guardedFetch, normalizeForecast, normalizeAir } from "./weather-tools.js";
72
+ export { normalizePlace, normalizePlaces, normalizeElevation, distanceKm, nearbyViewbox } from "./places-tools.js";
69
73
  export { createR2Client, R2_ACCESS_KEY_REF, R2_SECRET_KEY_REF } from "./files-tools.js";
70
74
  export { signRequest, presignGet } from "./sigv4.js";
71
75
  export { freeSlots } from "./events-tools.js";