@openparachute/agent 0.1.0 → 0.1.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/LICENSE +675 -21
  3. package/LICENSE-NANOCLAW-MIT +21 -0
  4. package/README.md +8 -1
  5. package/docs/design/2026-05-02-channel-policy-and-approval-routing.md +1 -1
  6. package/package.json +2 -1
  7. package/scripts/init-cli-agent.ts +2 -1
  8. package/scripts/init-first-agent.ts +2 -1
  9. package/scripts/seed-discord.ts +2 -1
  10. package/src/channels/api-translator.test.ts +306 -0
  11. package/src/channels/api-translator.ts +214 -0
  12. package/src/config.ts +23 -3
  13. package/src/container-runtime.test.ts +101 -1
  14. package/src/container-runtime.ts +76 -1
  15. package/src/db/connection.migrate.test.ts +35 -2
  16. package/src/db/connection.ts +40 -5
  17. package/src/index.ts +6 -1
  18. package/src/mcp/tools/channels.test.ts +126 -0
  19. package/src/mcp/tools/channels.ts +33 -98
  20. package/src/modules/mount-security/expand-path.test.ts +82 -0
  21. package/src/modules/mount-security/index.ts +21 -10
  22. package/src/modules/permissions/sender-approval.test.ts +171 -0
  23. package/src/secrets/index.ts +127 -21
  24. package/src/secrets/secrets.test.ts +301 -4
  25. package/src/session-manager.attachments.test.ts +171 -0
  26. package/src/session-manager.dup-skip.test.ts +173 -0
  27. package/src/session-manager.ts +22 -4
  28. package/src/types.ts +4 -1
  29. package/src/web/routes/channels-mga-detail.test.ts +49 -2
  30. package/src/web/routes/channels.ts +25 -203
  31. package/src/web/routes/secrets.test.ts +46 -1
  32. package/src/web/routes/secrets.ts +35 -0
  33. package/src/web/server.ts +34 -13
  34. package/src/web/services-manifest.test.ts +37 -9
  35. package/src/web/services-manifest.ts +14 -9
  36. package/web/ui/index.html +2 -2
  37. package/web/ui/src/App.tsx +1 -1
  38. package/web/ui/src/lib/api.test.ts +2 -2
  39. package/web/ui/src/lib/api.ts +40 -2
  40. package/web/ui/src/lib/auth.test.ts +214 -1
  41. package/web/ui/src/lib/auth.ts +79 -22
  42. package/web/ui/src/routes/ChannelWireDetail.test.tsx +2 -2
  43. package/web/ui/src/routes/ChannelWireDetail.tsx +1 -1
  44. package/web/ui/src/routes/GroupDetail.test.tsx +206 -0
  45. package/web/ui/src/routes/GroupDetail.tsx +126 -1
  46. package/web/ui/src/routes/MessagingGroupDetail.test.tsx +1 -1
  47. package/web/ui/src/routes/SecretsList.tsx +22 -1
  48. package/web/ui/src/routes/VaultDetail.test.tsx +2 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,54 @@
2
2
 
3
3
  All notable changes to parachute-agent will be documented in this file.
4
4
 
5
+ ## [0.1.2] - 2026-05-05
6
+
7
+ The first patch series after the 0.1.0 paraclaw → parachute-agent rename. Fourteen iterative cuts (rc.1 through rc.14) collapsed into one stable. No operator action required: every change is either a transparent fix, an additive UI affordance, or a behind-the-scenes test addition.
8
+
9
+ ### Fixed
10
+
11
+ - **Master-key migration: detect the both-exist split-state explicitly.** `migrateMasterKeyLocation` previously silent-no-op'd when both `<PARACHUTE_DIR>/claw/master.key` and `<PARACHUTE_DIR>/agent/master.key` existed — masking the case where an earlier 0.1.x boot generated a fresh key at the new path before the legacy was copied (so encrypted secrets sealed under the legacy key became undecryptable). The function now logs a `warn` with both paths and copy-pasteable recovery commands. Standalone scripts (`init-cli-agent`, `init-first-agent`, `seed-discord`) that ran `migrateCentralDbLocation` now also run `migrateMasterKeyLocation` before opening the DB, so a script-driven first touch no longer skips the key copy. Also: SPA browser title `<title>Paraclaw</title>` → `<title>Parachute Agent</title>` and two stale GitHub repo links pointing at the renamed-from `paraclaw` URL — small follow-ups to the 0.1.0 brand sweep that landed in the same cut.
12
+
13
+ - **Auto-retag the per-install container image when `INSTALL_SLUG` shifts (paraclaw#114).** `INSTALL_SLUG = sha1(process.cwd())[:8]`, so an operator dir-rename (the trigger that exposed this: `mv paraclaw parachute-agent`) flips the slug. Previously-built images carried the old slug; new container spawns went out under the new slug; `docker run` returned `code=125` ("image not found") and every Telegram message produced a silent crashloop. New `ensureContainerImage()` step at boot detects the mismatch and `docker tag`s any `parachute-agent-image-<peer-slug>:latest` it finds onto the expected name. Pre-0.1.0 `paraclaw-agent-<slug>:latest` peers also match (one cycle of compat). When no peer is on disk, the daemon now fails visibly at startup with an actionable error instead of crashlooping silently.
14
+
15
+ - **Inbound: extract attachment files only after the row commits (paraclaw#96).** `writeSessionMessage` previously decoded base64 attachments and wrote files to `inbox/<messageId>/` *before* `INSERT … ON CONFLICT(id) DO NOTHING` returned. Once duplicate dispatch became a warm code path (sender-approval replay, Telegram getUpdates retry, chat-sdk re-emit), a replay carrying the same `messages_in.id` but mutated bytes silently clobbered the on-disk file under the original message id while the DB row stayed unchanged — divergent state with no audit trail. Reordered: insert with raw inline-base64 content, check `inserted`, and only when `inserted === true` extract files and `UPDATE messages_in SET content = ?` with the path-replaced form. Disk state now stays strictly downstream of the row commit.
16
+
17
+ - **Wire-side `senderScope` vocabulary clash (paraclaw#94).** The wire vocab `'allowlist' | 'all'` shared the literal `'all'` with the DB-side `'all' | 'known'` — both meant "no sender filter", but the literal collision meant a grep-based rename of either side would silently break translation without a compile error. Renamed wire-side `'all'` → `'unrestricted'` so the two unions are now literal-disjoint; DB schema untouched (no migration). Touchpoints: HTTP + MCP translators, MCP `update-channel-wire` schema enum (now `['allowlist', 'unrestricted']`), `web/ui/src/lib/api.ts:SenderScope`, and the dropdown copy in `ChannelWireDetail.tsx`. Plus a defensive validation gate on the MCP handler — the SDK does not enforce `inputSchema` against `tools/call` arguments, so a stale-schema client sending the legacy `senderScope: 'all'` (or `ignoredMessagePolicy: 'accumulate'`, or a typo'd `engageMode`) would previously land past the rename gate, never match any branch, and silently no-op. Now explicitly rejected with a diagnostic error. **Breaking change to the API/MCP wire vocabulary** — pre-1.0, no operator-data risk.
18
+
19
+ - **Mount-security imports `HOME_DIR` from `src/config.ts` (paraclaw#99).** `expandPath` in `src/modules/mount-security/index.ts` previously called `process.env.HOME || os.homedir()` directly — the only remaining offender after the rest of the host's HOME-derived paths routed through `config.ts`. Now imports the canonical `HOME_DIR`, so a future precedence-rule refactor (e.g. add a `PARACHUTE_AGENT_HOME` override) is one edit upstream. Default behavior unchanged. Mount-allowlist's on-disk location intentionally stays at `<HOME>/.config/parachute-agent/` (operator-host policy, not per-install runtime state) — pinned with a regression test.
20
+
21
+ - **`putSecret` auto-seeds the owner assignment for scoped creates (paraclaw#127).** The default `agent_groups.secret_mode` is `selective` (migration 023). Before this fix, `putSecret(name, value, { agent_group_id })` inserted the `secrets` row without writing the matching `secret_assignments` row — leaving the row silently invisible to `resolveInjectableSecrets` (which gates on `secret_mode='all' OR assignment row exists`). The "+ New secret" → CredentialForm "free" mode in the SPA called only `putSecret` with no follow-up `setSecretAssignments`, so the standard create flow produced orphan rows whose values would never reach the agent container. Fix: `putSecret` writes the (id, owning_group) assignment row in the same transaction on INSERT (idempotent via `ON CONFLICT … DO NOTHING`); UPDATE/rotate leaves the assignment set alone (operator may have deliberately revoked an assignment, and a value rotation must not undo that).
22
+
23
+ - **SPA OAuth bootstrap — three narrowing fixes (paraclaw#136, #137, #138).** (1) Drop `vault:read vault:write` from `REQUESTED_SCOPES` — the agent SPA is self-contained, every vault flow already runs the per-vault re-consent pattern (`vault:<name>:admin` via `extraScopes`), so the broad bootstrap scopes were dead weight on the consent screen ("this app wants to read/write all your vaults" — wrong story for an SPA whose vault touches are narrowly per-vault and on-demand). (2) Regression-pin OAuth `client_name` in the registerClient body — the hub renders this string verbatim on its DCR consent screen; the 0.1.0 brand sweep renamed it from `Paraclaw web UI` to `Parachute Agent web UI`, this pins the wire-level test. (3) Re-register OAuth client when `redirect_uri` changes — the hub binds each DCR `client_id` to the redirect_uri it registered with; if the SPA's mount path changes (operator flips `PARACHUTE_AGENT_WEB_MOUNT` from `/claw/` → `/agent/`, or any custom remount), the cached client_id stops matching and `/oauth/authorize` errors out before the consent screen. Extended `ClientRecord` to `{ client_id, redirect_uri }`, compare in `ensureClient`, treat mismatch (or legacy missing-field record) as cache miss → re-register. Legacy records self-heal on first 0.1.x reload.
24
+
25
+ ### Changed
26
+
27
+ - **`services.json` self-registers `installDir` (paraclaw#115).** The agent's startup self-registration into `~/.parachute/services.json` now includes `installDir: process.cwd()` alongside the existing `name`/`port`/`paths`/`health`/`version` fields. Without it, hub's third-party-module lifecycle resolution (parachute-hub#84) couldn't locate the start command for `parachute restart agent` — the agent had a `.parachute/module.json` with `startCmd`, but hub needed `installDir` to know which checkout to drive.
28
+
29
+ - **GroupDetail "Secrets" panel — what the agent will receive at next session spawn (paraclaw#104).** `/agent/groups/:folder` now surfaces a read-only Secrets section showing the same set `resolveInjectableSecrets()` would inject into a new container, with three scope badges that explain *why* each row is included: `scoped` (owned by this group), `assigned` (global with explicit assignment row), `global` (global reaching the group only because `secret_mode='all'`). On a name collision the scoped row wins and reports `scoped`, mirroring the host's resolution rule. Click-through routes to `/secrets?edit=<id>` with a deep-link param for SecretEditor. New `GET /api/groups/:folder/secrets` endpoint (scope `agent:read`) — metadata only, never decrypts. Empty state distinguishes between mode='selective' (reads as "by design") and mode='all' (suggests creating a secret).
30
+
31
+ - **GroupDetail Secrets section — Retry button on error state (paraclaw#128).** Mirrors the existing AgentProviderSection pattern: the error banner now renders a Retry button bound to the same fetch callback so operators don't have to navigate away after a transient API failure.
32
+
33
+ - **Channel-wire translator extracted into a single shared module (paraclaw#123).** `src/web/routes/channels.ts` and `src/mcp/tools/channels.ts` each maintained their own copy of the `Api*` types, the `VALID_API_*` enum arrays, the `dbToApi*` translator pair, and the `ChannelWireView` shape. That duplication was the structural drift hazard paraclaw#94 surfaced concretely. Lifted everything into `src/channels/api-translator.ts`; the HTTP route file now owns only the transport layer, the MCP file only the tool-def plumbing. A future enum change touches one file and both surfaces pick it up automatically. (Behavioral side note: the inline MCP handler used to silently *drop* `engagePattern='.'` because the DB sentinel for `engageMode='all'` would round-trip back as `'all'` on the next read; the shared validator now hard-rejects that input identically on both surfaces. Use `'\\.'` to match a literal dot.)
34
+
35
+ - **Depersonalize test fixtures + comments.** Removed a real install-slug (`16f7e9e8`, the sha1 prefix of one operator's specific path) that had snuck into `src/container-runtime.test.ts` peer-image fixtures, plus a comment in `src/container-runtime.ts` that named the specific `mv` command from one operator's environment. Codebase should be operator-agnostic. Replaced with synthetic `cafef00d`. No behavior change.
36
+
37
+ ### Tests
38
+
39
+ - **Integration coverage for `writeSessionMessage` dup-skip + sender-approval replay (paraclaw#97).** The unit test added with #95 proved `insertMessage` returns `inserted=false` on a duplicate id, but the write-path side effects layered above it were never asserted at the integration level. New `src/session-manager.dup-skip.test.ts` (4 tests using real session DBs and real fs: dup dispatch doesn't bump `sessions.last_active`, log payload shape, N-concurrent same-id absorption to one row + one inbox file, distinct ids in the same burst still land), plus 2 new tests in `src/modules/permissions/sender-approval.test.ts` exercising the approval-replay chain end-to-end (file at `inbox/<id>:<agentGroupId>/photo.jpg`, byte-preserved on `original_message` mutation under accumulate-mode wiring). Stash-and-rerun confirmed both regression tests catch the underlying #92/#95/#96 bugs.
40
+
41
+ - **Parallel-equality lockstep guard for `resolveInjectableSecrets ↔ listInjectableSecretsForGroup` (paraclaw#129).** The two functions in `src/secrets/index.ts` are SQL-identical mirrors with a load-bearing doc-comment requiring lockstep edits — previously preserved only by careful reading and a #126-era reviewer note. Adds a `describe('… lockstep …')` block with an `expectLockstep` helper that calls both functions, asserts name-set equality, and walks each name through `getSecret` to verify the chosen row id (the `ORDER BY s.agent_group_id IS NULL` scoped-wins ordering) agrees with the plaintext returned. Five fixtures cover the rich-mix (scoped+all + global+assigned + global+mode=all + name collision), mode=selective, the orphaned-scoped corner, the unknown-agent-group selective-default path, and an empty store. Mechanical guard, no production code change.
42
+
43
+ ---
44
+
45
+ For per-rc commit-level detail of the 0.1.2 patch series, see `git log v0.1.1..v0.1.2 -- src/ web/ui/src/` or the merged PRs (#113 through #139).
46
+
47
+ ## [0.1.1] - 2026-05-05
48
+
49
+ ### Changed
50
+
51
+ - **License.** parachute-agent now declares **AGPL-3.0** in `package.json` and `LICENSE`, matching the rest of the Parachute ecosystem (vault, hub, scribe, notes). The original NanoClaw MIT license is preserved verbatim as `LICENSE-NANOCLAW-MIT` to honor the upstream copyright (Copyright (c) 2026 Gavriel — https://github.com/qwibitai/nanoclaw). Modifications and the combined work are AGPL-3.0; the original NanoClaw code remains MIT-licensed and obtainable from the upstream project. Resolves the npm "Proprietary" display that came from the missing `license` field at 0.1.0.
52
+
5
53
  ## [0.1.0] - 2026-05-05
6
54
 
7
55
  Renamed paraclaw → **parachute-agent**, joining the Parachute ecosystem's named-after-purpose convention (vault, notes, scribe, hub). The name on disk, in the npm registry, on the mount path, and on the wire all change. Operator data migrates automatically on first boot; tokens, container labels, and module manifests carry one cycle of back-compat.