mercury-agent 0.18.0-beta.0 → 0.18.1
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 -0
- package/docs/configuration.md +2 -2
- package/docs/extensions.md +73 -0
- package/docs/profile-guide.md +35 -0
- package/docs/skills-guide.md +31 -0
- package/docs/web-search.md +3 -1
- package/examples/extensions/archive/README.md +146 -0
- package/examples/extensions/archive/backends/drive.ts +486 -0
- package/examples/extensions/archive/backends/local.ts +180 -0
- package/examples/extensions/archive/backends/types.ts +82 -0
- package/examples/extensions/archive/capability.ts +252 -0
- package/examples/extensions/archive/gc.ts +225 -0
- package/examples/extensions/archive/index.ts +324 -0
- package/examples/extensions/archive/inflight.ts +65 -0
- package/examples/extensions/archive/manifest.ts +310 -0
- package/examples/extensions/archive/queue.ts +435 -0
- package/examples/extensions/archive/skill/SKILL.md +59 -0
- package/examples/extensions/morning/README.md +126 -0
- package/examples/extensions/morning/index.ts +174 -0
- package/examples/extensions/morning/lib/hosts.ts +75 -0
- package/examples/extensions/morning/lib/issue-token.ts +298 -0
- package/examples/extensions/morning/lib/morning.ts +515 -0
- package/examples/extensions/morning/lib/token.ts +282 -0
- package/examples/extensions/morning/skill/SKILL.md +144 -0
- package/examples/extensions/morning/skill/references/document-types.md +97 -0
- package/examples/extensions/morning/skill/references/errors.md +255 -0
- package/examples/extensions/web-search/brave.ts +324 -0
- package/examples/extensions/web-search/index.ts +53 -1
- package/examples/extensions/web-search/skill/SKILL.md +50 -12
- package/package.json +3 -3
- package/src/agent/container-env.ts +10 -0
- package/src/core/routes/capability.ts +4 -1
- package/src/core/runtime.ts +36 -0
- package/src/extensions/catalog.ts +18 -0
- package/src/extensions/hooks.ts +8 -2
- package/src/extensions/types.ts +46 -1
package/README.md
CHANGED
|
@@ -167,6 +167,7 @@ Install extensions from the dashboard (**Features** page), the CLI, or during se
|
|
|
167
167
|
```bash
|
|
168
168
|
# From the CLI
|
|
169
169
|
mercury add web-browser # Web browsing & search (uses Brave Search, no API key needed)
|
|
170
|
+
mercury add web-search # Brave Search API + site search skill (key stays on the host)
|
|
170
171
|
mercury add napkin # Obsidian-style knowledge vault
|
|
171
172
|
|
|
172
173
|
# Or pick a profile that bundles what you need
|
package/docs/configuration.md
CHANGED
|
@@ -146,7 +146,7 @@ agent:
|
|
|
146
146
|
env_passthrough: all # all (default) | claimed
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
-
- **`all`** — every `MERCURY_*` var except a fixed blocklist is passed into the container with the prefix stripped (`
|
|
149
|
+
- **`all`** — every `MERCURY_*` var except a fixed blocklist is passed into the container with the prefix stripped (`MERCURY_BILLING_API_KEY` → `BILLING_API_KEY`). Convenient, but blunt: a secret added to `.env` for one purpose reaches **every space's container**, regardless of who triggered the turn or whether that space has anything to do with it. Vars an extension declares `hostOnly` are excluded even here — `MERCURY_BRAVE_API_KEY` stays on the host once `web-search` is installed, which brokers Brave queries through `mrctl capability web-search`.
|
|
150
150
|
- **`claimed`** — only variables an extension declared via `mercury.env()` are passed, and only when the triggering caller holds that extension's permission. Undeclared variables stay on the host.
|
|
151
151
|
|
|
152
152
|
**Model-provider credentials are exempt** and pass in both modes (`MERCURY_ANTHROPIC_API_KEY`, `MERCURY_ANTHROPIC_OAUTH_TOKEN`, `MERCURY_GEMINI_API_KEY`, `MERCURY_GROQ_API_KEY`, and the rest of the provider list). pi reads them inside the container, and no extension declares them — without the exemption, `claimed` would leave the agent unable to reach any model. They remain subject to the blocklist.
|
|
@@ -156,7 +156,7 @@ Env: `MERCURY_CONTAINER_ENV_PASSTHROUGH`.
|
|
|
156
156
|
`claimed` is opt-in because it breaks setups that rely on blind passthrough for anything other than provider keys — API keys consumed by skills (search, TTS, scrapers) and any credential you added by hand. To migrate, declare those in an extension (see [extensions.md](extensions.md)) before switching. At startup with `all`, Mercury logs the vars it is passing that are neither declared nor provider credentials — names only, so a genuine outlier stands out:
|
|
157
157
|
|
|
158
158
|
```
|
|
159
|
-
Container env passthrough: all — these vars reach every space's container and are scoped to nothing. […] vars=
|
|
159
|
+
Container env passthrough: all — these vars reach every space's container and are scoped to nothing. […] vars=MERCURY_SCRAPER_API_KEY, MERCURY_BILLING_API_KEY
|
|
160
160
|
```
|
|
161
161
|
|
|
162
162
|
For secrets that only host-side hooks and jobs need, prefer `mercury.env({ from: "…", hostOnly: true })`, which keeps them out of containers in either mode. For credentials the agent should never hold at all, use a host-side capability handler (`mercury.capability()`), which runs the privileged call on the host and returns only the result.
|
package/docs/extensions.md
CHANGED
|
@@ -160,6 +160,7 @@ mercury.on("workspace_init", async (event, ctx) => {
|
|
|
160
160
|
| `workspace_init` | Space workspace created/ensured | No |
|
|
161
161
|
| `before_container` | About to spawn container | Yes |
|
|
162
162
|
| `after_container` | Container finished | Yes |
|
|
163
|
+
| `delivery` | Reply stored, attachments final | No |
|
|
163
164
|
|
|
164
165
|
Both `workspace_init` and `before_container` events include:
|
|
165
166
|
- `workspace` — absolute host path (for file operations on the host)
|
|
@@ -198,6 +199,34 @@ mercury.on("after_container", async (event, ctx) => {
|
|
|
198
199
|
});
|
|
199
200
|
```
|
|
200
201
|
|
|
202
|
+
#### `delivery` — what the user actually received
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
mercury.on("delivery", async (event, ctx) => {
|
|
206
|
+
// event.files — exactly what the adapter will attach
|
|
207
|
+
// event.withheld — what the media gate stripped
|
|
208
|
+
// event.userMessageId / event.assistantMessageId — both rows exist
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Use this, not `after_container`, whenever a hook needs to know **what was
|
|
213
|
+
delivered**. `after_container` fires *before* the media send gate and can still
|
|
214
|
+
rewrite the reply, so its file list is a proposal: a handler reading it cannot
|
|
215
|
+
tell whether the recipient was actually shown those files. `delivery` fires
|
|
216
|
+
after the gate and after the assistant row is written.
|
|
217
|
+
|
|
218
|
+
It is non-mutating — by the time it runs, the reply has been decided — and it
|
|
219
|
+
does not fire on paths where nothing reaches the user: a suppressed reply, or a
|
|
220
|
+
scheduler no-op. Handler errors are logged and isolated, so a failing handler
|
|
221
|
+
costs a log line and never the reply.
|
|
222
|
+
|
|
223
|
+
`event.withheld` exists so a handler can tell "withheld" from "never produced".
|
|
224
|
+
Never treat a withheld file as delivered: the gate replaces the file array
|
|
225
|
+
rather than annotating it, so this is the only place that distinction survives.
|
|
226
|
+
|
|
227
|
+
The `archive` extension is the worked example
|
|
228
|
+
([`examples/extensions/archive/`](../examples/extensions/archive/)).
|
|
229
|
+
|
|
201
230
|
### `mercury.job(name, def)`
|
|
202
231
|
|
|
203
232
|
Register a background job that runs on the host.
|
|
@@ -440,6 +469,50 @@ The skill is installed into the management space's own workspace on
|
|
|
440
469
|
dir mounted into *every* container, which would advertise the capability to
|
|
441
470
|
spaces that cannot use it.
|
|
442
471
|
|
|
472
|
+
## Morning / Green Invoice invoicing (`morning`)
|
|
473
|
+
|
|
474
|
+
`examples/extensions/morning/` issues tax documents through the
|
|
475
|
+
[Morning](https://morning.co) API. Second `mercury.capability()` adopter after
|
|
476
|
+
yahoo-mail, and the case that justifies rung 4: these credentials create legally
|
|
477
|
+
binding documents against a real business, so they never enter a container.
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
mercury add ./examples/extensions/morning
|
|
481
|
+
MERCURY_MORNING_CLIENT_ID=... # hostOnly
|
|
482
|
+
MERCURY_MORNING_CLIENT_SECRET=... # hostOnly
|
|
483
|
+
mrctl config set morning.environment sandbox # or production; sandbox is the default
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
The agent calls it with `mrctl capability morning <action> '<json>'`:
|
|
487
|
+
|
|
488
|
+
| Action | Body | Returns |
|
|
489
|
+
|--------|------|---------|
|
|
490
|
+
| `client-search` | `{name?, email?, page?, pageSize?}` | paginated clients |
|
|
491
|
+
| `client-create` | `{name, taxId?, emails?, …}` | the new client |
|
|
492
|
+
| `document-types` | `{lang?}` | `[{id, name}]`, localised |
|
|
493
|
+
| `document-preview` | the document body | `{file, issueToken}` — base64 PDF, nothing created |
|
|
494
|
+
| `document-create` | the same body **plus `issueToken`** | `{id, number, url}` |
|
|
495
|
+
| `document-search` | `{fromDate?, toDate?, type?, …}` | paginated documents |
|
|
496
|
+
| `document-links` | `{id}` | `{origin, he, en}` |
|
|
497
|
+
|
|
498
|
+
Two properties are enforced in the broker rather than requested in the skill:
|
|
499
|
+
|
|
500
|
+
- **Preview before issue.** `document-create` refuses without a single-use
|
|
501
|
+
`issueToken` minted by `document-preview` of the *identical* payload. The
|
|
502
|
+
token is bound to the payload hash, the environment, the space and the caller,
|
|
503
|
+
so a changed amount, a sandbox-previewed document presented against
|
|
504
|
+
production, another person's preview, or a retry after a successful create are
|
|
505
|
+
each refused. A prompt instruction is not a control when the model may be the
|
|
506
|
+
thing under attack.
|
|
507
|
+
- **`sensitive: true`.** In a group-linked space the runtime then requires
|
|
508
|
+
`security.sensitive_connections_allowed` plus per-caller confirmation. Set that
|
|
509
|
+
key or the space refuses every turn — the symptom otherwise reads as a broken
|
|
510
|
+
extension.
|
|
511
|
+
|
|
512
|
+
Slice 1 is invoices. Expenses are deferred: `POST /expenses` requires an
|
|
513
|
+
`accountingClassification` whose lookup endpoint is absent from the published
|
|
514
|
+
spec, and an uploaded file becomes a draft with no API to approve it.
|
|
515
|
+
|
|
443
516
|
## Examples
|
|
444
517
|
|
|
445
518
|
See [`examples/extensions/`](../examples/extensions/) for complete, working extensions ranging from minimal (charts — CLI + skill) to full-featured (napkin — hooks, jobs, config, widgets, KB distillation).
|
package/docs/profile-guide.md
CHANGED
|
@@ -134,6 +134,7 @@ reads top-down and the first sections frame the rest.
|
|
|
134
134
|
## Silence when to say nothing and how (NO_UPDATE, "אין חדש")
|
|
135
135
|
## Integrity about what was done "I wrote it" only after a successful write; who can change what
|
|
136
136
|
## Dedupe / memory routine which file to read before posting and append after
|
|
137
|
+
## Incoming files which uploads to keep, and where (see 4.7)
|
|
137
138
|
## Privacy "you may see it, you may not keep it" — never a denial of what is visible
|
|
138
139
|
## What this space does NOT do one line each; the reader's expectations, closed explicitly
|
|
139
140
|
## Overrides of platform text the explicit sentences (see 4.4)
|
|
@@ -248,6 +249,40 @@ is argued from memory. The football profile keeps the dates in
|
|
|
248
249
|
`decisions.md` instead (every `D-0NN` names the day and its `Revisit if:`),
|
|
249
250
|
which works as long as the file's section names match the decision ids.
|
|
250
251
|
|
|
252
|
+
### 4.7 Incoming files: say which uploads to keep
|
|
253
|
+
|
|
254
|
+
`inbox/` is a transit directory. It is swept after `MERCURY_INBOX_TTL_DAYS`
|
|
255
|
+
(7 by default), and the sweep also nulls the attachment reference in
|
|
256
|
+
`state.db` — so a week later the bot cannot produce the file *or* name it. No
|
|
257
|
+
runtime rule can decide which uploads matter, because only the conversation
|
|
258
|
+
knows. That makes it a profile rule, and a profile without one silently loses
|
|
259
|
+
customer material.
|
|
260
|
+
|
|
261
|
+
The rule that works, in one section:
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
## Incoming files
|
|
265
|
+
A file sent with no request attached, or with "keep this" / "use this from now
|
|
266
|
+
on", is reference material: move it to `knowledge/assets/<name>`, write a short
|
|
267
|
+
note at `knowledge/references/<slug>.md` saying what it is and when to use it,
|
|
268
|
+
and tell the person where you put it.
|
|
269
|
+
A file sent as part of a request (edit this, summarise this) is working
|
|
270
|
+
material: use it and leave it. It disappears after a week and that is fine.
|
|
271
|
+
Never say you have kept a file unless you moved it.
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Two things that rule gets right and the obvious version does not. **Silence
|
|
275
|
+
defaults to keeping**: most uploads arriving with no question are meant to be
|
|
276
|
+
kept, so an unclassified file should be shelved, not discarded. And the last
|
|
277
|
+
line closes the integrity gap — the same one §"Integrity about what was done"
|
|
278
|
+
exists for, applied to storage rather than to writes.
|
|
279
|
+
|
|
280
|
+
Files the bot **sends** are the mirror problem and are *not* a profile rule:
|
|
281
|
+
`outbox/` is swept after three days, and the `archive` extension is what keeps
|
|
282
|
+
them. Install it and enable `archive.enabled` per space; the agent then has
|
|
283
|
+
`mrctl capability archive list|get`. See
|
|
284
|
+
[`../examples/extensions/archive/README.md`](../examples/extensions/archive/README.md).
|
|
285
|
+
|
|
251
286
|
---
|
|
252
287
|
|
|
253
288
|
## 5. Accuracy: put the computation in code
|
package/docs/skills-guide.md
CHANGED
|
@@ -155,6 +155,37 @@ never be blanket-copied over from the source: the copy reverts the paths to
|
|
|
155
155
|
placeholders and the agent's commands break at runtime. Copy, substitute,
|
|
156
156
|
then `diff` and confirm the only difference is the placeholder.
|
|
157
157
|
|
|
158
|
+
### Reference material is not a skill, and does not live in `inbox/`
|
|
159
|
+
|
|
160
|
+
A logo, a brand guide, a price list, a corpus a customer uploads "so you answer
|
|
161
|
+
better" — none of these is a skill, and none of them survives where they land.
|
|
162
|
+
`inbox/` is swept after `MERCURY_INBOX_TTL_DAYS` (7 by default), and the sweep
|
|
163
|
+
also nulls the attachment reference in `state.db`, so the agent later cannot
|
|
164
|
+
even name the file it was sent.
|
|
165
|
+
|
|
166
|
+
Put reference material in the space workspace, which is never swept:
|
|
167
|
+
|
|
168
|
+
| What | Where | Why there |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| the bytes (logo, PDF, spreadsheet) | `knowledge/assets/<name>` | outside every TTL; mounted every run |
|
|
171
|
+
| a note describing it | `knowledge/references/<slug>.md` | the vault map indexes it, so the agent finds it the same way it finds everything else |
|
|
172
|
+
|
|
173
|
+
`knowledge/assets/` is a plain directory, deliberately not a skill directory —
|
|
174
|
+
pi scans only the two roots in the table above, and a binary in a skill dir
|
|
175
|
+
would be listed in the system prompt as though it were an instruction. The note
|
|
176
|
+
in `references/` is what makes the asset discoverable; write it the way you
|
|
177
|
+
would write a skill description, naming the situation the file is for.
|
|
178
|
+
|
|
179
|
+
Deciding *which* uploads become reference material is a **profile rule**, not
|
|
180
|
+
something the runtime can infer — see `profile-guide.md`. The rule that works in
|
|
181
|
+
practice is "a file sent with no request attached, or with 'keep this', is
|
|
182
|
+
reference material": most uploads carrying no question are meant to be kept, so
|
|
183
|
+
silence should default to keeping rather than to discarding.
|
|
184
|
+
|
|
185
|
+
Files the assistant **sends** are a separate problem with a separate answer: the
|
|
186
|
+
`archive` extension keeps them past the `outbox/` TTL and gives the agent
|
|
187
|
+
`mrctl capability archive list|get`. It does not cover inbox uploads, by design.
|
|
188
|
+
|
|
158
189
|
## 7. Checklist before you ship a skill
|
|
159
190
|
|
|
160
191
|
- [ ] `name` is lowercase-kebab, ≤64 chars; `description` ≤1024 chars, says
|
package/docs/web-search.md
CHANGED
|
@@ -10,7 +10,9 @@ mercury add web-browser
|
|
|
10
10
|
|
|
11
11
|
Or install from the dashboard's **Features** page (one click).
|
|
12
12
|
|
|
13
|
-
No API key is required. The extension uses [pinchtab](https://www.npmjs.com/package/pinchtab) to control a local headless Chromium and navigates to the public [Brave Search](https://search.brave.com) website — the same way a human would.
|
|
13
|
+
No API key is required for the browser path. The extension uses [pinchtab](https://www.npmjs.com/package/pinchtab) to control a local headless Chromium and navigates to the public [Brave Search](https://search.brave.com) website — the same way a human would.
|
|
14
|
+
|
|
15
|
+
The separate `web-search` extension (`mercury add web-search`) adds structured site search (flights, hotels, cars, apartments) on top of pinchtab and, for fast keyword and news lookups, exposes the Brave Search **API** as a host-side capability: set `MERCURY_BRAVE_API_KEY` in `.env` and the agent can run `mrctl capability web-search news '{"q":"..."}'` (or `web`). The key is declared `hostOnly`, so it never enters a container; the host makes the call, applies a per-space hourly cap (`brave_hourly_cap`, default 60), and returns trimmed results.
|
|
14
16
|
|
|
15
17
|
## What It Provides
|
|
16
18
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# archive
|
|
2
|
+
|
|
3
|
+
Keeps every file the assistant delivers, after `outbox/` is swept.
|
|
4
|
+
|
|
5
|
+
## The problem it solves
|
|
6
|
+
|
|
7
|
+
`outbox/` is a delivery pipe that gets used as a memory. The storage sweep
|
|
8
|
+
removes its files after `MERCURY_OUTBOX_TTL_DAYS` (3 by default), and in a space
|
|
9
|
+
with no repo behind it — every customer space — the deliverable is then gone for
|
|
10
|
+
good. A customer asking for last month's report cannot be answered.
|
|
11
|
+
|
|
12
|
+
Raising the TTL only postpones that and costs disk on a box that does not have
|
|
13
|
+
it. This extension adds the missing tier instead: a permanent, per-space,
|
|
14
|
+
content-addressed store, with an index the agent can search and a delete path
|
|
15
|
+
for a single item or a whole customer.
|
|
16
|
+
|
|
17
|
+
## How it decides what to keep
|
|
18
|
+
|
|
19
|
+
**Attachment is the classification.** The extension listens on `delivery`, which
|
|
20
|
+
fires once the reply is stored and the media gate has run, so it sees what was
|
|
21
|
+
committed for delivery. There is no tag for the agent to remember and no sweep
|
|
22
|
+
racing the TTL cleanup.
|
|
23
|
+
|
|
24
|
+
The event fires before the adapter's send returns, so a send that fails
|
|
25
|
+
afterwards — a dropped socket — leaves a file archived that never arrived.
|
|
26
|
+
That is deliberate: the alternative is archiving nothing until an
|
|
27
|
+
acknowledgement that some platforms never give, and over-keeping is the
|
|
28
|
+
recoverable direction.
|
|
29
|
+
|
|
30
|
+
What is therefore *not* archived, by design:
|
|
31
|
+
|
|
32
|
+
- files the media gate withheld — they were never delivered;
|
|
33
|
+
- files in `inbox/` that no run ever turned into a deliverable. Reference
|
|
34
|
+
material a customer wants kept (a logo, a brand guide, a price list) belongs
|
|
35
|
+
in `knowledge/assets/` with a note in `knowledge/references/`, which is a
|
|
36
|
+
profile rule, not this extension's job. See `docs/skills-guide.md`.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
mercury add ./examples/extensions/archive
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then enable it per space — it is **off by default**, so installing it changes
|
|
45
|
+
nothing until you say so:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
mrctl config set archive.enabled true
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
To turn it on for every space, set `archive.enabled` in the `@global` scope on
|
|
52
|
+
the dashboard, or in `mercury.yaml` under `extensions:`.
|
|
53
|
+
|
|
54
|
+
> On a deployment running an applicative profile, an extension is only reachable
|
|
55
|
+
> once its permission is in the profile's `member_permissions` (both the source
|
|
56
|
+
> YAML and the deployed `active-profile.json`). Without that the capability is
|
|
57
|
+
> silently unavailable to members.
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
All host-side and `hostOnly` — none of it enters a container.
|
|
62
|
+
|
|
63
|
+
| Variable | Default | Meaning |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `MERCURY_ARCHIVE_BACKEND` | `local` | `local` or `drive` |
|
|
66
|
+
| `MERCURY_ARCHIVE_LOCAL_DIR` | `<dataDir>/archive/blobs` | Where the local backend stores blobs |
|
|
67
|
+
| `MERCURY_ARCHIVE_DRIVE_FOLDER` | `Mercury Archive` | Root folder name in the connected Google account |
|
|
68
|
+
| `MERCURY_GWS_CREDENTIALS_JSON` | — | Required for `drive`; the same credential `gws` uses |
|
|
69
|
+
|
|
70
|
+
A backend that cannot be built disables the extension with an error in the log
|
|
71
|
+
and **never** breaks delivery. If you see `[archive] disabled — …` at startup,
|
|
72
|
+
that is this, and the reason is on the same line.
|
|
73
|
+
|
|
74
|
+
### Which backend
|
|
75
|
+
|
|
76
|
+
**`local`** works with nothing configured. Point `MERCURY_ARCHIVE_LOCAL_DIR` at
|
|
77
|
+
a mounted Storage Box and the bytes leave the VPS without any further change.
|
|
78
|
+
On a bare VPS disk it does *not* survive that disk — that is the backup
|
|
79
|
+
runbook's job (`docs/runbooks/backup-offbox.md`), and archive and backup are
|
|
80
|
+
deliberately different systems with different retention.
|
|
81
|
+
|
|
82
|
+
**`drive`** reuses the Google credential the `gws` extension already needs, so a
|
|
83
|
+
deployment with Google connected gains an off-box archive with no new vendor and
|
|
84
|
+
no new account. On a customer's own VPS with their own Google account this also
|
|
85
|
+
settles ownership: the files are in the customer's Drive, so if they leave they
|
|
86
|
+
keep their deliverables.
|
|
87
|
+
|
|
88
|
+
Two things to check before enabling `drive`:
|
|
89
|
+
|
|
90
|
+
- **Is the OAuth app published?** An app still in "Testing" issues refresh
|
|
91
|
+
tokens that expire after 7 days. The archive will keep retrying and logging
|
|
92
|
+
`invalid_grant`. Publish the app in the GCP console, or expect to re-consent
|
|
93
|
+
weekly.
|
|
94
|
+
- **Whose Drive is it?** On a shared deployment the credential is the operator's,
|
|
95
|
+
so every customer's deliverables land in the operator's Drive. That is a
|
|
96
|
+
contractual question, not a technical one.
|
|
97
|
+
|
|
98
|
+
## Erasure
|
|
99
|
+
|
|
100
|
+
One item, by an admin, from chat:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
mrctl capability archive forget '{"id":"<id>"}'
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Appends a tombstone and deletes the bytes unless another archived entry
|
|
107
|
+
references the same file. Permanent — on Drive this is `files.delete`, not the
|
|
108
|
+
trash.
|
|
109
|
+
|
|
110
|
+
A whole customer, by an operator, from the host:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
bun run scripts/archive-erase-space.ts <space-id> # dry run
|
|
114
|
+
bun run scripts/archive-erase-space.ts <space-id> --yes # do it
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Removes the backend prefix, the spool, the manifest and `ARCHIVE.md`. Dry-run by
|
|
118
|
+
default because it cannot be undone.
|
|
119
|
+
|
|
120
|
+
**After restoring from a backup**, re-run the drain and reconcile so tombstones
|
|
121
|
+
written after the snapshot are re-applied. Both are idempotent. Without this
|
|
122
|
+
step a restore silently resurrects erased files — the failure the EDPB's 2026
|
|
123
|
+
enforcement report singles out.
|
|
124
|
+
|
|
125
|
+
## Operational notes
|
|
126
|
+
|
|
127
|
+
- **Never enable object lock or bucket versioning** on an archive store. They
|
|
128
|
+
make per-item erasure impossible, which is the one thing this must be able to
|
|
129
|
+
do.
|
|
130
|
+
- Uploads are queued: the delivery hook only hashes and copies to a host-side
|
|
131
|
+
spool, and a job every 60s does the upload with retries. A backend outage
|
|
132
|
+
grows the spool and logs; it never delays or breaks a reply.
|
|
133
|
+
- A weekly reconcile compares the store against the manifest. Orphaned blobs are
|
|
134
|
+
deleted. A live entry with no bytes is logged at **error** level and cannot be
|
|
135
|
+
repaired automatically — it means an archived file can no longer be delivered.
|
|
136
|
+
|
|
137
|
+
## Layout
|
|
138
|
+
|
|
139
|
+
| File | What it is |
|
|
140
|
+
|---|---|
|
|
141
|
+
| `index.ts` | Wiring: permission, config, `delivery` hook, jobs, capability |
|
|
142
|
+
| `manifest.ts` | The append-only JSONL log and `ARCHIVE.md` rendering |
|
|
143
|
+
| `queue.ts` | The spool and the drain, including the blob-before-manifest rule |
|
|
144
|
+
| `gc.ts` | Erasure and the reconcile job |
|
|
145
|
+
| `backends/` | `local` and `drive`, behind one interface |
|
|
146
|
+
| `capability.ts` | `list` / `get` / `forget`, and the admin gate on `forget` |
|