mercury-agent 0.24.1 → 0.24.2-beta.2
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 +1 -1
- package/docs/auth/whatsapp.md +6 -4
- package/docs/configuration.md +5 -0
- package/docs/container-lifecycle.md +2 -2
- package/docs/extensions.md +1 -0
- package/docs/goals/conversation-review/decisions.md +352 -0
- package/docs/goals/conversation-review/goal.md +150 -0
- package/docs/goals/conversation-review/roadmap.md +94 -0
- package/docs/goals/rehearsal-bench/roadmap.md +1 -1
- package/docs/goals/space-links/decisions.md +443 -0
- package/docs/goals/space-links/goal.md +141 -0
- package/docs/goals/space-links/roadmap.md +129 -0
- package/docs/live-testing.md +34 -3
- package/docs/pending-verification.md +176 -1
- package/docs/plans/{flat-0916 → archive/2026-09-28-flat-0916}/plan.md +4 -4
- package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/plan.md +2 -2
- package/docs/plans/archive/2026-10-02-media-bugs-0928/briefs/S1.md +59 -0
- package/docs/plans/archive/2026-10-02-media-bugs-0928/briefs/S2.md +64 -0
- package/docs/plans/archive/2026-10-02-media-bugs-0928/briefs/S3.md +61 -0
- package/docs/plans/archive/2026-10-02-media-bugs-0928/decisions.md +6 -0
- package/docs/plans/archive/2026-10-02-media-bugs-0928/plan.md +111 -0
- package/docs/plans/archive/2026-10-02-media-bugs-0928/sessions/S1.md +34 -0
- package/docs/plans/archive/2026-10-02-media-bugs-0928/sessions/S2.md +43 -0
- package/docs/plans/archive/2026-10-02-media-bugs-0928/sessions/S3.md +35 -0
- package/docs/plans/{sensitive-bugs-0922 → archive/2026-10-02-sensitive-bugs-0922}/plan.md +2 -2
- package/docs/plans/archive/2026-10-02-sensitive-bugs-0922/sessions/S1.md +35 -0
- package/docs/plans/archive/2026-10-02-sensitive-bugs-0922/sessions/S2.md +30 -0
- package/docs/plans/archive/2026-10-02-sensitive-bugs-0922/sessions/S3.md +39 -0
- package/docs/plans/archive/2026-10-02-sensitive-bugs-0922/sessions/S4.md +38 -0
- package/docs/plans/archive/2026-10-02-sensitive-bugs-0922/sessions/S5.md +37 -0
- package/docs/plans/general-0928/briefs/S1.md +66 -0
- package/docs/plans/general-0928/briefs/S2.md +64 -0
- package/docs/plans/general-0928/briefs/S3.md +58 -0
- package/docs/plans/general-0928/briefs/S4.md +58 -0
- package/docs/plans/general-0928/briefs/S5.md +58 -0
- package/docs/plans/general-0928/decisions.md +8 -0
- package/docs/plans/general-0928/plan.md +107 -0
- package/docs/plans/search-bugs-0930/briefs/S1.md +73 -0
- package/docs/plans/search-bugs-0930/briefs/S2.md +75 -0
- package/docs/plans/search-bugs-0930/briefs/S3.md +77 -0
- package/docs/plans/search-bugs-0930/briefs/S4.md +75 -0
- package/docs/plans/search-bugs-0930/briefs/S5.md +74 -0
- package/docs/plans/search-bugs-0930/briefs/S6.md +77 -0
- package/docs/plans/search-bugs-0930/decisions.md +8 -0
- package/docs/plans/search-bugs-0930/plan.md +165 -0
- package/docs/plans/search-bugs-0930/sessions/S1.md +28 -0
- package/docs/plans/search-bugs-0930/sessions/S2.md +27 -0
- package/docs/plans/search-bugs-0930/sessions/S3.md +40 -0
- package/docs/plans/search-bugs-0930/sessions/S5.md +39 -0
- package/docs/plans/search-bugs-0930/sessions/S6.md +37 -0
- package/docs/plans/search-quality-0930/briefs/S12.md +69 -0
- package/docs/plans/search-quality-0930/briefs/S7.md +76 -0
- package/docs/plans/search-quality-0930/briefs/S9.md +68 -0
- package/docs/plans/search-quality-0930/decisions.md +11 -0
- package/docs/plans/search-quality-0930/plan.md +107 -0
- package/docs/plans/unattended-1003/briefs/S1.md +92 -0
- package/docs/plans/unattended-1003/briefs/S2.md +92 -0
- package/docs/plans/unattended-1003/briefs/S3.md +90 -0
- package/docs/plans/unattended-1003/briefs/S4.md +92 -0
- package/docs/plans/unattended-1003/briefs/S5.md +92 -0
- package/docs/plans/unattended-1003/briefs/S6.md +94 -0
- package/docs/plans/unattended-1003/briefs/S7.md +92 -0
- package/docs/plans/unattended-1003/briefs/S8.md +90 -0
- package/docs/plans/unattended-1003/briefs/S9.md +92 -0
- package/docs/plans/unattended-1003/decisions.md +7 -0
- package/docs/plans/unattended-1003/plan.md +153 -0
- package/docs/scheduler.md +5 -1
- package/docs/web-search.md +13 -7
- package/examples/extensions/napkin/distill-input.ts +27 -6
- package/examples/extensions/napkin/prompts/kb-distillation.md +3 -1
- package/examples/extensions/office-365-mail/README.md +3 -2
- package/examples/extensions/office-365-mail/index.ts +35 -1
- package/examples/extensions/office-365-mail/lib/graph.ts +158 -11
- package/examples/extensions/office-365-mail/lib/save.ts +140 -0
- package/examples/extensions/office-365-mail/skill/SKILL.md +15 -1
- package/examples/extensions/pinchtab/bin/pinchtab_ensure +129 -0
- package/examples/extensions/pinchtab/bin/pinchtab_nav +50 -0
- package/examples/extensions/pinchtab/bin/pinchtab_session_inject.cjs +184 -0
- package/examples/extensions/pinchtab/index.ts +41 -237
- package/examples/extensions/pinchtab/skill/SKILL.md +7 -110
- package/examples/extensions/voice-transcribe/index.ts +192 -50
- package/examples/extensions/voice-transcribe/skill/SKILL.md +2 -0
- package/examples/extensions/web-search/brave.ts +396 -18
- package/examples/extensions/web-search/index.ts +122 -22
- package/examples/extensions/web-search/ledger.ts +156 -0
- package/examples/extensions/web-search/skill/SKILL.md +30 -6
- package/examples/extensions/web-search/skill/references/zap.md +148 -0
- package/examples/profiles/football-reporter/config.yaml +11 -2
- package/examples/profiles/tel-aviv-flat-search/config.yaml +3 -2
- package/package.json +4 -4
- package/src/adapters/whatsapp-media.ts +262 -40
- package/src/adapters/whatsapp.ts +1 -1
- package/src/agent/container-entry.ts +348 -36
- package/src/agent/container-error.ts +39 -2
- package/src/agent/container-runner.ts +162 -23
- package/src/agent/pi-failure-class.ts +36 -6
- package/src/agent/pi-jsonl-parser.ts +151 -33
- package/src/agent/provider-auth-rejection.ts +279 -0
- package/src/agent/user-error-messages.ts +26 -0
- package/src/bridges/discord.ts +7 -0
- package/src/bridges/slack.ts +13 -1
- package/src/bridges/teams.ts +29 -16
- package/src/bridges/telegram.ts +13 -0
- package/src/bridges/whatsapp.ts +6 -0
- package/src/cli/mercury.ts +85 -0
- package/src/cli/mrctl-http.ts +67 -0
- package/src/cli/mrctl.ts +14 -12
- package/src/config.ts +5 -3
- package/src/core/api-types.ts +5 -0
- package/src/core/api.ts +9 -1
- package/src/core/auth.ts +1 -1
- package/src/core/caller-token.ts +32 -1
- package/src/core/handler.ts +11 -1
- package/src/core/media.ts +216 -16
- package/src/core/operator-alerts.ts +204 -4
- package/src/core/outbox.ts +125 -12
- package/src/core/routes/capability.ts +2 -2
- package/src/core/routes/config-builtin.ts +27 -0
- package/src/core/routes/console.ts +7 -0
- package/src/core/routes/dashboard.ts +10 -0
- package/src/core/routes/tasks.ts +11 -0
- package/src/core/runtime.ts +705 -113
- package/src/core/space-links.ts +273 -0
- package/src/core/system-messages.ts +45 -2
- package/src/core/task-scheduler.ts +115 -5
- package/src/env-dump/build-deps.ts +2 -55
- package/src/extensions/catalog.ts +5 -3
- package/src/extensions/context.ts +4 -0
- package/src/extensions/image-builder.ts +265 -65
- package/src/extensions/reserved.ts +6 -0
- package/src/extensions/types.ts +30 -0
- package/src/ops/shadow-diff.ts +1027 -0
- package/src/preflight/binary-version.ts +103 -0
- package/src/preflight/build-deps.ts +2 -0
- package/src/preflight/checks/credential.ts +16 -9
- package/src/preflight/checks/host-deps.ts +107 -10
- package/src/preflight/deps.ts +5 -0
- package/src/profile/space-profile.ts +21 -0
- package/src/server.ts +56 -1
- package/src/storage/db.ts +603 -0
- package/src/storage/pi-auth.ts +92 -8
- package/src/text/reporter-lint.ts +77 -30
- package/docs/plans/flat-0916/briefs/S1.md +0 -55
- package/docs/plans/flat-0916/briefs/S2.md +0 -55
- /package/docs/plans/{flat-0916 → archive/2026-09-28-flat-0916}/decisions.md +0 -0
- /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S1.md +0 -0
- /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S2.md +0 -0
- /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S3.md +0 -0
- /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S4.md +0 -0
- /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S5.md +0 -0
- /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S6.md +0 -0
- /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S7.md +0 -0
- /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S8.md +0 -0
- /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/decisions.md +0 -0
- /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/sessions/S1.md +0 -0
- /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/sessions/S2.md +0 -0
- /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/sessions/S3.md +0 -0
- /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/sessions/S4.md +0 -0
- /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/sessions/S5.md +0 -0
- /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/sessions/S6.md +0 -0
- /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/sessions/S7.md +0 -0
- /package/docs/plans/{sensitive-bugs-0922 → archive/2026-10-02-sensitive-bugs-0922}/briefs/S1.md +0 -0
- /package/docs/plans/{sensitive-bugs-0922 → archive/2026-10-02-sensitive-bugs-0922}/briefs/S2.md +0 -0
- /package/docs/plans/{sensitive-bugs-0922 → archive/2026-10-02-sensitive-bugs-0922}/briefs/S3.md +0 -0
- /package/docs/plans/{sensitive-bugs-0922 → archive/2026-10-02-sensitive-bugs-0922}/briefs/S4.md +0 -0
- /package/docs/plans/{sensitive-bugs-0922 → archive/2026-10-02-sensitive-bugs-0922}/briefs/S5.md +0 -0
- /package/docs/plans/{sensitive-bugs-0922 → archive/2026-10-02-sensitive-bugs-0922}/decisions.md +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Roadmap: Conversation Review
|
|
2
|
+
|
|
3
|
+
**Goal**: [conversation-review](goal.md)
|
|
4
|
+
**Last updated**: 2026-09-28
|
|
5
|
+
|
|
6
|
+
## Milestones
|
|
7
|
+
|
|
8
|
+
### Milestone 1: Daily judge on the local bot
|
|
9
|
+
> On the local bot, a daily scan judges every exchange of the last 24 h and
|
|
10
|
+
> writes a redacted report on the box. The owner gets a DM digest.
|
|
11
|
+
|
|
12
|
+
| ID | Story | Slug | Depends on | Status |
|
|
13
|
+
|----|-------|------|------------|--------|
|
|
14
|
+
| M1.1 | Read a window of exchanges per space from `state.db`, read-only | review-exchange-reader | — | backlog |
|
|
15
|
+
| M1.2 | `redactIdentifiers` + fail-closed `assertNoIdentifiers` in core | redact-identifiers-with-gate | — | backlog |
|
|
16
|
+
| M1.3 | `conversation-review` extension: daily job, batched no-tools judge, validated findings | conversation-review-daily-judge | M1.1 | blocked |
|
|
17
|
+
| M1.4 | Report (`report.md` + `report.json`) on the box and the owner DM digest | conversation-review-report-and-digest | M1.2, M1.3 | blocked |
|
|
18
|
+
|
|
19
|
+
**Checkpoint:** on the local bot, after deploying and enabling:
|
|
20
|
+
|
|
21
|
+
1. In a test space, the owner provokes one bad answer: a question the bot answers
|
|
22
|
+
wrongly or refuses without reason. They trigger a scan with the extension's
|
|
23
|
+
run-now hook (or wait for 04:00).
|
|
24
|
+
2. `~/whatsapp-bot/.../reviews/<date>-daily/report.md` exists and flags that
|
|
25
|
+
exchange with a kind, a scope, a target layer and the right message id.
|
|
26
|
+
3. `grep -E` for a phone number, `@s.whatsapp.net`, `@lid` and the owner's name
|
|
27
|
+
in the report returns nothing.
|
|
28
|
+
4. A DM arrives with the finding count, the top three findings and the report
|
|
29
|
+
path.
|
|
30
|
+
5. The report's footer lists spaces and exchanges judged, anything skipped with
|
|
31
|
+
the reason, spawns, wall clock and cost. The journal shows the scan never
|
|
32
|
+
stalled a live reply.
|
|
33
|
+
|
|
34
|
+
### Milestone 2: Both bots, into the repo
|
|
35
|
+
> The Tagula bot runs the same scan on its release. `/f-review-intake` brings both
|
|
36
|
+
> bots' reports into `docs/reviews/` and triages each finding against what is
|
|
37
|
+
> already documented or landed before any of it becomes work (D-011).
|
|
38
|
+
|
|
39
|
+
| ID | Story | Slug | Depends on | Status |
|
|
40
|
+
|----|-------|------|------------|--------|
|
|
41
|
+
| M2.1 | `scripts/review-intake.ts` + `/f-review-intake`: pull, re-gate, then verify → relate → decide per finding, owner approves, ledger | review-intake-to-repo | M1.4 | blocked |
|
|
42
|
+
| M2.2 | Enable on Tagula: release, config, `include_clients` gate, tagula-agents runbook | conversation-review-on-tagula | M1.4 | blocked |
|
|
43
|
+
|
|
44
|
+
**Checkpoint:** a daily report from each bot lands in `docs/reviews/`. The
|
|
45
|
+
intake triage table then shows, each row citing the paths it matched:
|
|
46
|
+
- at least one finding re-judged as fine and proposed as `reject`;
|
|
47
|
+
- one proposed as `attach` to an existing open item;
|
|
48
|
+
- one checked against a landed item, as `fixed-not-deployed` or `revisit`;
|
|
49
|
+
- a finding raised again on a later day, under a different slug, reads
|
|
50
|
+
`duplicate` and follows the earlier decision.
|
|
51
|
+
|
|
52
|
+
Only the rows the owner approved were applied, and every row is in
|
|
53
|
+
`docs/reviews/ledger.json`.
|
|
54
|
+
|
|
55
|
+
### Milestone 3: Weekly deep pass and a feedback loop
|
|
56
|
+
> A weekly strong-model synthesis clusters recurring issues across spaces and
|
|
57
|
+
> measures the daily judge's recall. The owner's verdicts on findings sharpen the
|
|
58
|
+
> judge.
|
|
59
|
+
|
|
60
|
+
| ID | Story | Slug | Depends on | Status |
|
|
61
|
+
|----|-------|------|------------|--------|
|
|
62
|
+
| M3.1 | Weekly job: cluster 7 days of findings, space → global promotion, recall spot check | conversation-review-weekly-synthesis | M1.4 | blocked |
|
|
63
|
+
| M3.2 | The M2.1 ledger feeds the judge: suppressed fingerprints and counter-examples from rejected findings go back to each bot; precision appears in the weekly report | review-verdict-feedback | M2.1, M3.1 | blocked |
|
|
64
|
+
|
|
65
|
+
**Checkpoint:** the first weekly report lists at least one cluster that spans two
|
|
66
|
+
or more spaces, and states the measured recall of the daily judge. After a week
|
|
67
|
+
of intake verdicts, it also states precision, and a rejected false positive is
|
|
68
|
+
not re-raised.
|
|
69
|
+
|
|
70
|
+
### Future milestones (to be decomposed when M3 completes)
|
|
71
|
+
- **Trace-aware analysis:** link a reply to its run trace (message id in the
|
|
72
|
+
trace name, the system prompt recorded) so a finding can cite the tool call
|
|
73
|
+
that failed. This builds on `trace-records-system-prompt`.
|
|
74
|
+
- **Friction signals as extra evidence:** corrections, rephrasing, "you didn't
|
|
75
|
+
answer". Relates to `repeated-style-corrections-become-preferences`.
|
|
76
|
+
- **Cross-bot clustering** at intake, and a view of trends over time.
|
|
77
|
+
- **Proposal → rehearsal:** replay the flagged exchange against a proposed
|
|
78
|
+
profile change on the shadow bench (`rehearsal-bench`, `mercury smoke`) and
|
|
79
|
+
attach the before/after answer to the finding.
|
|
80
|
+
- **Drafted fixes:** a finding becomes a pre-filled bug or idea doc, or a draft
|
|
81
|
+
`AGENTS.md` diff, still approved by a human.
|
|
82
|
+
|
|
83
|
+
## Dependency Graph
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
M1.1 ─┐
|
|
87
|
+
├→ M1.3 ─┐
|
|
88
|
+
M1.2 ─┼────────┴→ M1.4 ─┬→ M2.1 ─┐
|
|
89
|
+
│ ├→ M2.2 ├→ M3.2
|
|
90
|
+
│ └→ M3.1 ─┘
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
M1.1 and M1.2 can run in parallel. M2.1, M2.2 and M3.1 can run in parallel once
|
|
94
|
+
M1.4 lands.
|
|
@@ -48,7 +48,7 @@ matrix in §2 or edits it; `ops-preflight` refuses the exact command of
|
|
|
48
48
|
| M1.1 | `shadow: boolean` config (`MERCURY_SHADOW`), absent = live: no adapter constructed, `consoleUrl` unset, dashboard banner + `shadow=1` on every log line; negative tests for each branch | shadow-mode-flag | — | **done** 2026-09-02 — [spec](../../archive/rehearsal/2026-09-02-shadow-mode-flag.md), PR #68 |
|
|
49
49
|
| M1.2 | `mercury shadow snapshot`: `VACUUM INTO` + copy of `spaces/ global/ extensions/ model-capabilities.yaml` into `~/mercury-shadow/<ts>/`, `SNAPSHOT.json` (source commit, live HEAD, time), `current` symlink, shadow `.env` derived by a script (copy + override lines, never printed) and a `mercury.yaml` with `runtime.data_dir` and `server.port` rewritten; the `.env` writer emits `MERCURY_SHADOW=1` (only `true`/`1` enable the mode) and the shadow **refuses to start** when `runtime.data_dir` is not under `~/mercury-shadow/` — the flag alone protects people, not the live DB (M1.1 review, 2026-09-02) | shadow-snapshot | M1.1 | **shipped** 2026-09-03 — [spec](../../archive/rehearsal/2026-09-03-shadow-snapshot.md), PR #71, deployed 2026-09-03. `mercury shadow snapshot\|prune`, `src/ops/shadow-root.ts` + `src/ops/shadow-snapshot.ts`; the data-dir refusal closes the M1.1 residual |
|
|
50
50
|
| M1.3 | `mercury shadow up|down|status|logs`: transient user unit via `systemd-run --user --unit mercury-shadow`, `--src` defaults to `~/mercury-src`; `down` prints the run cost from the shadow's `token_usage`; refuses `up` when the live `mercury` unit is not the only thing on 8787 or when the snapshot is older than `--max-age`. **From M1.2's plan:** the unit must set `HOME` (the data-dir guard refuses on an empty home), and `up` must record its snapshot path so `shadow prune` skips the running one. **Added 2026-09-03 (D-011, D-012):** (a) *prerequisite, first commit of the story* — every container the runner creates (agent, probes) carries a `mercury.instance=<id>` label derived from the data dir, and `cleanupOrphans` **always** filters on it — no id, no sweep; negative test deletes the filter and watches a foreign container get listed; (b) `--src` also accepts an installed package (`mercury-agent` under npm, run as root with `XDG_RUNTIME_DIR=/run/user/0`), so the same command works on a fleet box; (c) `up` refuses below `--min-free-mem` / `--min-free-disk`, while `task_runs` has an unfinished row, or while a cron is due within the run window; never builds an image (missing derived image → refuse, name the tag); binds 8788 to loopback | shadow-service | M1.2 | **done** 2026-09-07 — [spec](../../archive/rehearsal/2026-09-07-shadow-service.md). **Three corrections found in the code while building it:** the roadmap's "refuse while `task_runs` has an unfinished row" could not be written as specified (`finished_at` is NOT NULL and rows are written at completion, so the table never shows a run in progress) — it is a *running container* carrying the live instance id instead; "binds 8788 to loopback" was **dropped**, because under `runc` agent containers reach the host API over TCP at `host.docker.internal:<port>` and a loopback bind would break agent runs in the shadow only (a bind-host option is B-2, live and shadow alike); and the label had to cover a fourth container site, `preflight/probe-container.ts` |
|
|
51
|
-
| M1.4 | `mercury shadow diff [--space <id>]`: live snapshot vs shadow now — `space_config` / `space_preferences` / `tasks` / `space_roles` / `mutes` rows with `updated_by` before→after, `messages` added (count + first lines), workspace tree by hash (`MEMORY.md`, `knowledge/**`, `AGENTS.md`), `extension_state` keys. Output is the report a migration rehearsal is judged on | shadow-diff | M1.2 |
|
|
51
|
+
| M1.4 | `mercury shadow diff [--space <id>]`: live snapshot vs shadow now — `space_config` / `space_preferences` / `tasks` / `space_roles` / `mutes` rows with `updated_by` before→after, `messages` added (count + first lines), workspace tree by hash (`MEMORY.md`, `knowledge/**`, `AGENTS.md`), `extension_state` keys. Output is the report a migration rehearsal is judged on | shadow-diff | M1.2 | **done 2026-10-01** ([archive](../../archive/rehearsal/2026-10-01-shadow-diff.md)) — `diffShadow` in `src/ops/shadow-diff.ts` is the library call M3.2 / M4.1 reuse |
|
|
52
52
|
| M1.5 | `longview` gains a `file` target and shadow mode forces it; the transcript shows the page path. `capability` route refuses non-`readOnly` actions in shadow mode | longview-file-target | M1.1 | backlog |
|
|
53
53
|
| M1.3b | **Shadow admin list is chosen, never inherited** (added 2026-09-13, D-015, from bug `shadow-env-keys-does-not-override-mercury-admins`): `shadow snapshot --admins <id,…>` (default empty) is what `renderShadowEnv` writes as `MERCURY_ADMINS`; `SNAPSHOT.json.shadow.adminCount` records the count, `snapshot`/`up`/`status` print it (never the ids); `up` refuses a snapshot whose manifest records no count. Landed with the shadow-up hardening track (B1 of the 2026-09-12 bench-and-gate round), which also closed: the live project read from the unit's `WorkingDirectory` / `SNAPSHOT.json` (never cwd, disagreement refuses), the cron gate's third state (unknown denies, none allows), `mercury run` on `process.execPath` + the unit `PATH` composed from the live unit / `--path` (never the shell's), and the CLI tests' scoped user bus | shadow-admins | M1.3 | **done** 2026-09-13 — post-mortems under `docs/debug/{moderate,minor}/2026-09-1{2,3}-shadow-*` and `…-mercury-run-spawns-a-bare-bun-…` |
|
|
54
54
|
| M1.3c | **Shadow-own model credential** (added 2026-09-15, from bug `shadow-snapshot-auth-json-goes-stale-when-the-live-bot-rotates-its-refresh-token`): the snapshot stops copying `global/auth.json`; `up` points `MERCURY_AUTH_PATH` at a shadow-only file the owner logs in to once (`mercury auth login` against the shadow root), so the shadow never holds the live refresh token — neither dies when the other rotates. D-016's principle ("a shadow answers to no live credential") extended from the API secrets to the model credential. The `Bun.serve` 10 s stream death on `/api/shadow/simulate` (bug `shadow-simulate-stream-dies-at-bun-serve-10s-idle-timeout`) — the other reason a bench run needs a person watching — is **not** this story's: it is fixed directly as plan `bugs-0915` S1 (corrected 2026-09-15), and the bug's shape (a) mitigation (`up` re-copies the live credential, `--dry-run` gates on it) ships as `bugs-0915` S4; this story owns shape (c) | shadow-own-credential | M1.3 | backlog |
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
# Decisions: Space Links
|
|
2
|
+
|
|
3
|
+
**Goal**: [space-links](goal.md)
|
|
4
|
+
|
|
5
|
+
> Revised 2026-09-26 at checkpoint 1. The owner rejected the first draft's
|
|
6
|
+
> closed capability catalogue: *"from a management space [we should be able] to
|
|
7
|
+
> change any setting, read any message in any other space, and in general to
|
|
8
|
+
> have any control we would have if we were an admin in the target space. The
|
|
9
|
+
> target space context must not be affected at all by this, but the source space
|
|
10
|
+
> context can fill with data from the target space when required (not always)."*
|
|
11
|
+
> D-005 is rewritten, D-011 to D-014 are new, and D-003, D-004, D-006, D-008 and
|
|
12
|
+
> D-009 are amended to match.
|
|
13
|
+
>
|
|
14
|
+
> Revised again 2026-09-26 (second round of checkpoint 1). The owner dropped
|
|
15
|
+
> transitivity: *"should we give up transitivity? maybe it just complicates
|
|
16
|
+
> things and can cause problems. any link should be easily visible and
|
|
17
|
+
> editable."* D-003 is rewritten. The owner also asked to summarize a client
|
|
18
|
+
> space's day, test the output it would give a client, and edit its profile,
|
|
19
|
+
> all from one management space, with no conversation linked to each client and
|
|
20
|
+
> no leak from management into the client. D-015 and D-016 are new.
|
|
21
|
+
|
|
22
|
+
## D-001: No new stack; SQLite tables in `Db.migrate()`, Hono routes, `mrctl` subcommand
|
|
23
|
+
- **Category:** tech-stack
|
|
24
|
+
- **Decided:**
|
|
25
|
+
- Two new tables, `space_links` and `space_link_audit`, added to the
|
|
26
|
+
`CREATE TABLE IF NOT EXISTS` block in `src/storage/db.ts`.
|
|
27
|
+
- One route module, `src/core/routes/links.ts`, mounted at `/links`, plus one
|
|
28
|
+
change to the auth middleware in `src/core/api.ts` (D-011).
|
|
29
|
+
- One `mrctl links` subcommand group and one global `mrctl --space` flag.
|
|
30
|
+
- No new dependency.
|
|
31
|
+
- **Alternatives considered:**
|
|
32
|
+
- Links as `space_config` keys: no directed edge, no audit, and a
|
|
33
|
+
per-space key cannot express the target side.
|
|
34
|
+
- A links extension, the way `overview` is an extension: it would put the
|
|
35
|
+
authorization boundary in extension code that any space can disable or
|
|
36
|
+
shadow.
|
|
37
|
+
- **Reasoning:** Cross-space authorization is core. The `character` route
|
|
38
|
+
(b3c1841) and `mrctl model` (0bbb63a) are recent, working templates for exactly
|
|
39
|
+
this shape.
|
|
40
|
+
- **Revisit if:** links need to span two Mercury instances. That would need an
|
|
41
|
+
inter-host transport, which is out of scope. Owner, 2026-09-28: "for now we
|
|
42
|
+
will only handle links between spaces on the same bot. The management space
|
|
43
|
+
will be on the Tagula bot. Links between bots on separate servers is deferred
|
|
44
|
+
for later." Parked as `docs/ideas/cross-bot-space-links.md`.
|
|
45
|
+
- **Used by:** all stories
|
|
46
|
+
|
|
47
|
+
## D-002: A link is one directed edge `holder → target`; "two-sided" is two edges
|
|
48
|
+
- **Category:** architecture
|
|
49
|
+
- **Decided:**
|
|
50
|
+
- Each `space_links` row is one directed edge. The **holder** side gains the
|
|
51
|
+
edge's access level over the **target** side.
|
|
52
|
+
- `mrctl links create A B --both` writes two rows, A→B and B→A, with the same
|
|
53
|
+
level, in one transaction.
|
|
54
|
+
- Asymmetric pairs are two separate `create` calls.
|
|
55
|
+
- The optional conversation filter (`target_conversation_id`) narrows the
|
|
56
|
+
**target** end only.
|
|
57
|
+
- **Alternatives considered:**
|
|
58
|
+
- One row with a `bidirectional` flag: cannot express the asymmetric case
|
|
59
|
+
(management administers a client space; the client sees nothing back).
|
|
60
|
+
- `none | a→b | b→a | both` per axis on one row, as the idea doc proposed:
|
|
61
|
+
every check would need to know which end is "a".
|
|
62
|
+
- **Reasoning:**
|
|
63
|
+
- Every check becomes the same question: is there an edge from the holder to
|
|
64
|
+
the target (or to `*`), and at what level?
|
|
65
|
+
- The direction words are fixed as **holder** and **target**, never "from/to"
|
|
66
|
+
or "a/b". Data flows target→holder; access points holder→target.
|
|
67
|
+
- **Revisit if:** a real case needs one revoke to take out both halves of a pair
|
|
68
|
+
atomically often enough to warrant a `pair_id`.
|
|
69
|
+
- **Used by:** all stories
|
|
70
|
+
|
|
71
|
+
## D-003: Links are not transitive; only a direct edge (or `*`) grants access
|
|
72
|
+
- **Category:** architecture
|
|
73
|
+
- **Decided:**
|
|
74
|
+
- The holder reaches a target only through an active edge
|
|
75
|
+
`holder → target` or `holder → *`. B's own links give A nothing.
|
|
76
|
+
- When several direct edges match (a `*` edge and a specific one, or a
|
|
77
|
+
filtered and an unfiltered one), the **strongest** level wins.
|
|
78
|
+
- Computed on every call. Never materialized, never cached.
|
|
79
|
+
- **Alternatives considered:**
|
|
80
|
+
- Transitive, weakest edge on a path wins, best path wins (this decision's
|
|
81
|
+
first version, from the intake answer "links are transitive"). The owner
|
|
82
|
+
withdrew it on 2026-09-26: "maybe it just complicates things and can cause
|
|
83
|
+
problems. any link should be easily visible and editable."
|
|
84
|
+
- A per-edge `transitive` flag. It keeps every problem of transitivity for
|
|
85
|
+
the edges that set it.
|
|
86
|
+
- **Reasoning:**
|
|
87
|
+
- Every access that exists is one row in `mrctl links list`, and changing or
|
|
88
|
+
revoking it is one command on that row. With transitivity, an access could
|
|
89
|
+
exist on no row at all, and a link created for one purpose (X→mgmt) could
|
|
90
|
+
silently hand X the reach of mgmt's wildcard.
|
|
91
|
+
- The owner's case needs no chain: one management space with a `*` edge, or
|
|
92
|
+
one edge per client.
|
|
93
|
+
- The resolver becomes one SELECT with no loop, no hop cap and no path to
|
|
94
|
+
audit.
|
|
95
|
+
- **Revisit if:** a real case needs A to reach C through B, and writing the
|
|
96
|
+
A→C row is worse than what transitivity would cost. Nothing in the goal needs
|
|
97
|
+
it today.
|
|
98
|
+
- **Used by:** space-link-graph-and-grant-resolver and every story that checks a
|
|
99
|
+
grant
|
|
100
|
+
|
|
101
|
+
## D-004: One resolver, literal arguments; the caller's own role in the holder still bounds them
|
|
102
|
+
- **Category:** pattern
|
|
103
|
+
- **Decided:**
|
|
104
|
+
- Every link-mediated request goes through
|
|
105
|
+
`resolveLinkGrant(db, holderSpaceId, targetSpaceId)` in
|
|
106
|
+
`src/core/space-links.ts`, called from exactly one place: the act-as step of
|
|
107
|
+
the auth middleware (D-011). No route calls it on its own.
|
|
108
|
+
- The holder is always `auth.spaceId` from the verified caller token. The
|
|
109
|
+
target is the explicit `x-mercury-act-as` value.
|
|
110
|
+
- The caller must hold `links.act` in the **holder** (default: admin). A caller
|
|
111
|
+
who is not an admin (or `system`) in the holder is capped at `read` in the
|
|
112
|
+
target, whatever the link says.
|
|
113
|
+
- **Alternatives considered:**
|
|
114
|
+
- A parameterised `checkPerm(c, perm, targetSpace)` sprinkled per route. Every
|
|
115
|
+
route would need its own cross-space branch, and they would drift.
|
|
116
|
+
- **Reasoning:**
|
|
117
|
+
- One implementation, one call site: read, control, send and files cannot
|
|
118
|
+
disagree about what a link allows.
|
|
119
|
+
- A link never turns a member of the management space into an admin of a
|
|
120
|
+
client space.
|
|
121
|
+
- **Revisit if:** never, without a security review.
|
|
122
|
+
- **Used by:** all stories
|
|
123
|
+
|
|
124
|
+
## D-005: A link carries an access level: `read` or `admin`
|
|
125
|
+
- **Category:** scope
|
|
126
|
+
- **Decided:**
|
|
127
|
+
- Each edge carries one level. Absent, unknown or unparsable denies.
|
|
128
|
+
|
|
129
|
+
| Level | In the target, the holder may… |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `read` | Everything an admin can **see** and nothing they can change: search messages, list conversations and tasks, get config, prefs, roles, permissions, mutes and the model chain, and (M2) list and read workspace files, including `MEMORY.md` and the knowledge vault. Enforced per route: each `ACT_AS_ROUTES` entry is marked `read` or `admin`, and a `read` grant reaches only the `read` entries (D-011). |
|
|
132
|
+
| `admin` | Exactly what the target's `admin` role may do (`getRolePermissions(db, target, "admin")`): prefs, config, tasks, roles and permissions, mutes, model switch, rename, stop, compact, clear, send, and (M2) write workspace files and run extension actions. |
|
|
133
|
+
|
|
134
|
+
- What stays out, and why:
|
|
135
|
+
- **The Bot Character.** It is not per-space. A global admin already edits it
|
|
136
|
+
from any space with `mrctl character`, so there is nothing for a link to
|
|
137
|
+
grant.
|
|
138
|
+
- **Creating or changing links.** Global admin only (D-006). An `admin`
|
|
139
|
+
link into B does not let the holder link B to anything.
|
|
140
|
+
- **Anything global-admin-only** (`/character`, `/broadcast`, the `?all`
|
|
141
|
+
views). These check `callerId`, not the space, so a link cannot reach them.
|
|
142
|
+
- **Alternatives considered:**
|
|
143
|
+
- The first draft's closed nine-capability catalogue (`read.messages`,
|
|
144
|
+
`control.prefs`, …). Rejected by the owner on 2026-09-26: it left out roles,
|
|
145
|
+
memory and knowledge, and made "act as an admin there" into nine separate
|
|
146
|
+
grants plus a per-route implementation of each.
|
|
147
|
+
- A link naming a target role (`--as member`). Role names and permissions are
|
|
148
|
+
per-space and editable, so the minimum rule of D-003 would have to compare
|
|
149
|
+
permission sets across spaces. Two fixed levels keep the rule total.
|
|
150
|
+
- **Reasoning:** The owner's requirement is literally "whatever an admin of the
|
|
151
|
+
target could do". Reusing the target's own admin permission set means there is
|
|
152
|
+
no second list to keep in step with the permission system. `read` exists
|
|
153
|
+
because peer spaces (the 2026-09-24 TV-shortlist case) need to look without
|
|
154
|
+
being able to touch.
|
|
155
|
+
- **Revisit if:** a real case needs a middle level (for example "may change
|
|
156
|
+
prefs but not roles"). A third level slots between the two; it never widens
|
|
157
|
+
either.
|
|
158
|
+
- **Used by:** all stories
|
|
159
|
+
|
|
160
|
+
## D-006: Only a global admin creates, updates or revokes; target-side admins can always see inbound reach
|
|
161
|
+
- **Category:** architecture
|
|
162
|
+
- **Decided:**
|
|
163
|
+
- `create`, `update` and `revoke` are gated on `isGlobalAdmin(callerId)`, the
|
|
164
|
+
same as `routes/character.ts`.
|
|
165
|
+
- `list`, `inspect` and `audit` need the new `links.list` permission (default:
|
|
166
|
+
admin) in the caller's space. They show:
|
|
167
|
+
- edges where the caller's space is holder or target;
|
|
168
|
+
- the **inbound** reach (every space with a direct or `*` edge to this one,
|
|
169
|
+
and at what level) and the outbound reach.
|
|
170
|
+
- Acting across a link needs `links.act` (default: admin) in the holder (D-004).
|
|
171
|
+
- **Alternatives considered:**
|
|
172
|
+
- A space admin on both ends consents. Rejected by the owner on 2026-09-26.
|
|
173
|
+
- **Reasoning:**
|
|
174
|
+
- Creating a link grants access to someone else's conversations.
|
|
175
|
+
- Asymmetric visibility is acceptable; unaccountable visibility is not.
|
|
176
|
+
- **Revisit if:** customer-operated deployments need a space admin to create
|
|
177
|
+
links inside their own tenancy.
|
|
178
|
+
- **Used by:** mrctl-links-admin-commands, act-as-linked-space
|
|
179
|
+
|
|
180
|
+
## D-007: Disclosure is recorded, not enforced
|
|
181
|
+
- **Category:** scope
|
|
182
|
+
- **Decided:**
|
|
183
|
+
- `space_links.disclosure` is a free-text field set at create or update with
|
|
184
|
+
`--disclosure "<how and when the target side was told>"`.
|
|
185
|
+
- It is shown by `list` and `inspect`. Nothing is posted to the target side
|
|
186
|
+
automatically (which D-012 would forbid anyway).
|
|
187
|
+
- **Alternatives considered:**
|
|
188
|
+
- A required automatic notice.
|
|
189
|
+
- Leaving disclosure out entirely.
|
|
190
|
+
- **Reasoning:** Owner decision, 2026-09-26. Recording it keeps the question
|
|
191
|
+
visible without deciding it in code.
|
|
192
|
+
- **Revisit if:** a customer deployment or a legal review requires notice. The
|
|
193
|
+
field is then the natural gate.
|
|
194
|
+
- **Used by:** space-link-graph-and-grant-resolver, mrctl-links-admin-commands
|
|
195
|
+
|
|
196
|
+
## D-008: Every link-mediated request is audited in its own table
|
|
197
|
+
- **Category:** architecture
|
|
198
|
+
- **Decided:**
|
|
199
|
+
- `space_link_audit` gets one row per:
|
|
200
|
+
- `create`, `update`, `revoke` of a link;
|
|
201
|
+
- act-as request (D-011), successful or refused after the grant check, with
|
|
202
|
+
method, route, a redacted body summary, response status, and the id of
|
|
203
|
+
the edge that granted it.
|
|
204
|
+
- Rows survive `revoke` and `deleteSpace`.
|
|
205
|
+
- The audit table is host bookkeeping. It is never part of any space's prompt
|
|
206
|
+
(D-012).
|
|
207
|
+
- **Alternatives considered:**
|
|
208
|
+
- Logger lines only. The journal rotates and cannot be queried by the target's
|
|
209
|
+
admin.
|
|
210
|
+
- Per-route before/after capture. Twelve route modules would each need it; the
|
|
211
|
+
request body plus status is uniform and answers "who did what". Before/after
|
|
212
|
+
is added only where the route already returns it.
|
|
213
|
+
- **Reasoning:** The target's admin must be able to answer "who read or changed
|
|
214
|
+
what in my space". Today there is no config-change audit anywhere.
|
|
215
|
+
- **Revisit if:** audit volume from read requests becomes material. In that case,
|
|
216
|
+
aggregate reads per run.
|
|
217
|
+
- **Used by:** all stories
|
|
218
|
+
|
|
219
|
+
## D-009: `overview` stays as it is for now; it becomes a wildcard `read` link later
|
|
220
|
+
- **Category:** scope
|
|
221
|
+
- **Decided:**
|
|
222
|
+
- The `overview` extension keeps its management-space gate during this goal.
|
|
223
|
+
- With D-013, "management holds `read` over `*`" is exactly what `overview`
|
|
224
|
+
does. Porting it is a follow-up after M1 is live, not part of this goal.
|
|
225
|
+
- **Reasoning:** Migrating a working, live-verified capability inside the same
|
|
226
|
+
goal is risk without payoff.
|
|
227
|
+
- **Revisit if:** M1 ships. File the port as an idea then.
|
|
228
|
+
- **Used by:** —
|
|
229
|
+
|
|
230
|
+
## D-010: Naming: "space link", table `space_links`, verb group `links`
|
|
231
|
+
- **Category:** pattern
|
|
232
|
+
- **Decided:**
|
|
233
|
+
- In prose the new concept is always a **space link**.
|
|
234
|
+
- Code uses `SpaceLink` / `space_links` / `mrctl links` / `mrctl --space`.
|
|
235
|
+
- The existing "conversation linked to a space" (`conversations.space_id`,
|
|
236
|
+
`/spaces link`) is unchanged.
|
|
237
|
+
- **Alternatives considered:**
|
|
238
|
+
- "Bridge" is taken by `src/bridges/`. "Relation" is a word nobody uses when
|
|
239
|
+
they talk about the feature.
|
|
240
|
+
- **Reasoning:** The owner and every idea doc already say "link". The collision
|
|
241
|
+
is managed by always qualifying the word.
|
|
242
|
+
- **Revisit if:** users confuse `/spaces link` with `mrctl links` in practice.
|
|
243
|
+
- **Used by:** all stories
|
|
244
|
+
|
|
245
|
+
## D-011: Acting in a linked space is one middleware step, not a route per feature
|
|
246
|
+
- **Category:** architecture
|
|
247
|
+
- **Decided:**
|
|
248
|
+
- `mrctl --space <target> <any command>` sends `x-mercury-act-as: <target>`.
|
|
249
|
+
- The auth middleware in `src/core/api.ts`, after it has verified the caller
|
|
250
|
+
in the holder, runs the act-as step once:
|
|
251
|
+
1. `links.act` in the holder, or 403;
|
|
252
|
+
2. the target exists (`getSpace`, never `ensureSpace`), else the same 403;
|
|
253
|
+
3. the method and route are on the `ACT_AS_ROUTES` allowlist, each entry
|
|
254
|
+
marked `read` or `admin`, else 400;
|
|
255
|
+
4. `resolveLinkGrant(holder, target)` grants a level, else 403; a `read`
|
|
256
|
+
grant on an entry marked `admin` is also 403;
|
|
257
|
+
5. the auth context becomes `{ callerId, spaceId: target, role, via }`,
|
|
258
|
+
where `role` is always `admin` and `via` carries the holder, the level
|
|
259
|
+
and the granting edge id;
|
|
260
|
+
6. one audit row is written after the handler returns.
|
|
261
|
+
- Every existing route (`/messages`, `/prefs`, `/config`, `/tasks`, `/roles`,
|
|
262
|
+
`/permissions`, `/mutes`, `/model`, `/conversations`, `/spaces/current`,
|
|
263
|
+
`/control`, `/send`) then works against the target **unchanged**, because
|
|
264
|
+
they already read `auth.spaceId` and `checkPerm`.
|
|
265
|
+
- **Alternatives considered:**
|
|
266
|
+
- A `links read`, `links prefs`, … verb per capability (the first draft). Each
|
|
267
|
+
one re-implements an existing route and they drift.
|
|
268
|
+
- **Reasoning:** "Anything an admin of the target could do" is the existing API
|
|
269
|
+
surface evaluated in the target. Swapping the space once, at the boundary,
|
|
270
|
+
gets all of it with one check.
|
|
271
|
+
- **Revisit if:** a route needs to know it is being called across a link beyond
|
|
272
|
+
what `auth.via` tells it.
|
|
273
|
+
- **Used by:** act-as-linked-space, link-workspace-files, link-extension-actions
|
|
274
|
+
|
|
275
|
+
## D-012: The target's context is never touched by a link; the holder's fills only on demand
|
|
276
|
+
- **Category:** architecture
|
|
277
|
+
- **Decided:**
|
|
278
|
+
- **Target side.** A link, and any read through it, leaves the target
|
|
279
|
+
byte-identical: no prompt block, no notice, no row in `messages` or `members`,
|
|
280
|
+
no container spawned. The act-as step never calls `resolveRole` on the
|
|
281
|
+
target, because that upserts a member row.
|
|
282
|
+
- What *does* change the target is only an explicit admin action the holder
|
|
283
|
+
asked for (a preference set, a task created, a message sent, a session
|
|
284
|
+
cleared). That is the point of `admin`, and each is audited.
|
|
285
|
+
- **Holder side.** Nothing about linked spaces is injected into the holder's
|
|
286
|
+
prompt automatically: no per-space list, no digest. One static paragraph in
|
|
287
|
+
the platform block, gated on `links.act`, says that other spaces may be
|
|
288
|
+
reachable, that `mrctl links list` shows which, and that `mrctl --space` is
|
|
289
|
+
used only when the user asks about another space. Content from a target
|
|
290
|
+
enters the holder's context only as the output of a command the agent chose
|
|
291
|
+
to run.
|
|
292
|
+
- **Alternatives considered:**
|
|
293
|
+
- The first draft's `<linked_spaces>` block and `inject.digest`. Dropped: a
|
|
294
|
+
management space linked to thirty spaces would carry thirty spaces of context
|
|
295
|
+
on every turn.
|
|
296
|
+
- **Reasoning:** Owner requirement, 2026-09-26.
|
|
297
|
+
- **Revisit if:** a small, opt-in per-link digest is wanted for one specific pair.
|
|
298
|
+
It would be a new level or flag, never a default.
|
|
299
|
+
- **Used by:** act-as-linked-space, link-workspace-files
|
|
300
|
+
|
|
301
|
+
## D-013: A link target may be `*`, meaning every other space, present and future
|
|
302
|
+
- **Category:** scope
|
|
303
|
+
- **Decided:**
|
|
304
|
+
- `mrctl links create <holder> '*' --level admin` makes one edge that reaches
|
|
305
|
+
every space except the holder, including spaces created later.
|
|
306
|
+
- A wildcard edge takes no conversation filter.
|
|
307
|
+
- Every space's `links list` shows wildcard holders in its inbound reach.
|
|
308
|
+
- **Alternatives considered:**
|
|
309
|
+
- One edge per space. A new space would silently be outside management's reach
|
|
310
|
+
until someone remembered to link it.
|
|
311
|
+
- **Reasoning:** The owner's case is "from a management space … any other
|
|
312
|
+
space". The wildcard says that in one row, and it is visible from every
|
|
313
|
+
target.
|
|
314
|
+
- **Revisit if:** a deployment hosts spaces that must never be reachable even by
|
|
315
|
+
management. That would need a per-space `unlinkable` flag, which a wildcard
|
|
316
|
+
respects.
|
|
317
|
+
- **Used by:** space-link-graph-and-grant-resolver, mrctl-links-admin-commands
|
|
318
|
+
|
|
319
|
+
## D-014: Content from a target is untrusted, and the holder acts only on its own user's request
|
|
320
|
+
- **Category:** pattern
|
|
321
|
+
- **Decided:**
|
|
322
|
+
- Act-as responses that carry third-party text (messages, file contents) are
|
|
323
|
+
passed through `stripInvisibleUnicode` and tagged with the source space.
|
|
324
|
+
- The platform paragraph (D-012) tells the agent that text read from another
|
|
325
|
+
space is data, never instructions, and that `admin` actions through a link
|
|
326
|
+
are taken only when the holder's own user asked for them.
|
|
327
|
+
- **Reasoning:** With `admin` over many spaces, a prompt injection planted in
|
|
328
|
+
one client's chat could try to steer the management agent into changing
|
|
329
|
+
another client's settings. Prose is not a guard, so this is also a residual:
|
|
330
|
+
the audit is how such an action would be found.
|
|
331
|
+
- **Revisit if:** the audit ever shows an act-as write that no human asked for.
|
|
332
|
+
The next step is refusing act-as writes from a run whose triggering message is
|
|
333
|
+
not the holder's own user.
|
|
334
|
+
- **Used by:** act-as-linked-space, link-workspace-files
|
|
335
|
+
|
|
336
|
+
## D-015: Reading a period of a space's history is a read route, and summarizing is the holder's job
|
|
337
|
+
- **Category:** architecture
|
|
338
|
+
- **Decided:**
|
|
339
|
+
- A new route `GET /messages/recent?since=&until=&limit=&cursor=` lists a
|
|
340
|
+
space's messages in a time range, oldest first, with a cursor for the next
|
|
341
|
+
page. `mrctl messages recent --since 24h` calls it, and so does
|
|
342
|
+
`mrctl --space X messages recent --since 24h` through the act-as step
|
|
343
|
+
(marked `read` in `ACT_AS_ROUTES`).
|
|
344
|
+
- It serves the owner's "summarize everything from the past day in client X".
|
|
345
|
+
The management agent pages through the output and writes the summary. For a
|
|
346
|
+
long day it can hand the paging to a subagent, so the raw messages stay out
|
|
347
|
+
of the management conversation and only the summary comes back.
|
|
348
|
+
- The route is useful inside a space too ("what did we talk about
|
|
349
|
+
yesterday?"), so it is gated on its own `messages.read` permission
|
|
350
|
+
(default: admin), not on `compact`.
|
|
351
|
+
- **Alternatives considered:**
|
|
352
|
+
- Summarize on the host and return only the summary. That is a host-side
|
|
353
|
+
model call, which needs its own system prompt and model leg (the pi-spawn
|
|
354
|
+
rule in `CLAUDE.md`) and its own budget, for what the agent already does.
|
|
355
|
+
- Reuse `/messages/search` with an empty query. Search is newest-first,
|
|
356
|
+
capped at 200 and has no time range, so a day is not addressable with it.
|
|
357
|
+
- **Reasoning:** One read primitive the agent can page through covers "the past
|
|
358
|
+
day", "last week" and "since the last summary", and it adds nothing to the
|
|
359
|
+
holder's context until it is called (D-012).
|
|
360
|
+
- **Revisit if:** summaries of very busy spaces keep exhausting the holder's
|
|
361
|
+
context even with a subagent. A host-side digest per space per day would then
|
|
362
|
+
be the next step.
|
|
363
|
+
- **Used by:** messages-recent-range, act-as-linked-space
|
|
364
|
+
|
|
365
|
+
## D-016: Asking as another space is one turn of that space's agent on a throwaway copy of its state
|
|
366
|
+
- **Category:** architecture
|
|
367
|
+
- **Decided:**
|
|
368
|
+
- `mrctl --space X ask "<message>" [--as member|admin]` runs one agent
|
|
369
|
+
turn **as space X would run it**: X's `AGENTS.md`, preferences, memory,
|
|
370
|
+
character, model, history and tools. The reply comes back to the holder as
|
|
371
|
+
command output. No conversation of X is involved and nothing is sent to
|
|
372
|
+
anyone.
|
|
373
|
+
- It serves two requests with one mechanism:
|
|
374
|
+
- **testing**: "how would X answer 'what are your opening hours?'";
|
|
375
|
+
- **delegating**: "act as if you're in X and search for leads for a story
|
|
376
|
+
about aliens". The turn really runs X's search with X's instructions and
|
|
377
|
+
tools, so the answer is the one X's bot would give, not the management
|
|
378
|
+
agent's imitation of it.
|
|
379
|
+
- The management agent passes the reply on as it came back, labelled with the
|
|
380
|
+
space. It does not rewrite it in its own voice (D-017).
|
|
381
|
+
- Read-only tools (web search, fetch, reading X's files) work in the turn.
|
|
382
|
+
Only writes and sends are held back, below.
|
|
383
|
+
- The turn runs on a **throwaway copy**: a temporary copy of X's workspace
|
|
384
|
+
and session, deleted after the turn. X's `messages`, session, `MEMORY.md`,
|
|
385
|
+
tasks and preferences are unchanged afterwards, whatever the turn tried.
|
|
386
|
+
- Inside the turn every `mrctl` write and every outbound send is
|
|
387
|
+
refused, and the refusals are listed in the output ("would have
|
|
388
|
+
sent", "would have set"). Extensions that act on an outside account (gws,
|
|
389
|
+
yahoo-mail, tradestation) get no credentials in the turn.
|
|
390
|
+
- The only input from the management space is the message itself. The
|
|
391
|
+
turn never sees the management conversation, and the management
|
|
392
|
+
agent sees only the reply and the refusal list.
|
|
393
|
+
- Nothing is kept in X by default: leads found by a delegated search are not
|
|
394
|
+
added to X's memory or files. Whether a `--keep` option should let the
|
|
395
|
+
turn's results persist in X is open (it would put the request into X's
|
|
396
|
+
record, which D-012 otherwise forbids).
|
|
397
|
+
- `ask` is marked `admin` in `ACT_AS_ROUTES`. It changes nothing, but it
|
|
398
|
+
runs X's agent and spends tokens on X's model leg.
|
|
399
|
+
- **Alternatives considered:**
|
|
400
|
+
- A test conversation linked to each client space. That is what the owner
|
|
401
|
+
asked to avoid, and a turn there does write to X's history and session.
|
|
402
|
+
- The rehearsal-bench shadow (`docs/goals/rehearsal-bench/`). It has full
|
|
403
|
+
fidelity (real ingress, triggers, a copy of the whole data dir), but it is
|
|
404
|
+
a second service started by an operator from a shell. It stays the tool for
|
|
405
|
+
testing a deploy or a trigger. `ask` is the everyday tool from chat.
|
|
406
|
+
- The management agent imitating X after reading X's `AGENTS.md`. It would
|
|
407
|
+
run on management's model, prompt and tools, so the answer would not be
|
|
408
|
+
X's, which is what the owner asked for ("genuinely give a response as if
|
|
409
|
+
the bot in that space was asked it").
|
|
410
|
+
- **Reasoning:** "Test the output it would give a client" and "act as if you're
|
|
411
|
+
in X" both need X's real context and tools and must leave X untouched. A copy
|
|
412
|
+
that is thrown away gives both, and the refusals make the "would have done"
|
|
413
|
+
side visible without doing it.
|
|
414
|
+
- **Revisit if:** an `ask` reply differs from the live reply to the same
|
|
415
|
+
message in a way that matters. The likely cause is ingress (triggers,
|
|
416
|
+
mentions), and the answer is the shadow, not a more faithful `ask`.
|
|
417
|
+
- **Used by:** link-ask-as-space
|
|
418
|
+
|
|
419
|
+
## D-017: The owner speaks plain language; the agent picks the command
|
|
420
|
+
- **Category:** pattern
|
|
421
|
+
- **Decided:**
|
|
422
|
+
- `mrctl --space`, `messages recent` and `ask` are the agent's tools, not the
|
|
423
|
+
owner's interface. The owner says "what leads did you find in Phishkin's
|
|
424
|
+
space in the last 48 hours?" or "act as if you're in Phishkin's space and
|
|
425
|
+
…", and the agent chooses the command.
|
|
426
|
+
- The routing rules live in the one platform paragraph gated on `links.act`
|
|
427
|
+
(D-012): resolve a named space with `mrctl links list` (tolerating
|
|
428
|
+
misspellings, asking when two match); answer "what happened in X" from X's
|
|
429
|
+
record, stating the space and period covered; answer "act as X" with
|
|
430
|
+
`ask` and pass the reply on unchanged.
|
|
431
|
+
- Every milestone checkpoint is run with plain-language messages only, and
|
|
432
|
+
the trace is read to confirm which command the agent chose.
|
|
433
|
+
- **Alternatives considered:**
|
|
434
|
+
- A chat slash command per operation. The owner asked for plain language.
|
|
435
|
+
- Injecting the list of reachable spaces into the prompt so names resolve
|
|
436
|
+
without a call. Rejected by D-012: it grows with every linked space.
|
|
437
|
+
- **Reasoning:** An accurate answer to "what did X find" depends on reading X's
|
|
438
|
+
record, not on the management agent's recollection; the paragraph tells it
|
|
439
|
+
where each kind of record lives.
|
|
440
|
+
- **Revisit if:** live checks show the agent answering about another space
|
|
441
|
+
without running a command (guessing), or picking the wrong space. The next
|
|
442
|
+
step is a skill with worked examples.
|
|
443
|
+
- **Used by:** act-as-linked-space, messages-recent-range, link-ask-as-space
|