@openparachute/agent 0.1.0
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/.claude/scheduled_tasks.lock +1 -0
- package/.claude/settings.json +5 -0
- package/.claude/skills/add-atomic-chat-tool/SKILL.md +243 -0
- package/.claude/skills/add-atomic-chat-tool/atomic-chat-mcp-stdio.ts +229 -0
- package/.claude/skills/add-codex/SKILL.md +161 -0
- package/.claude/skills/add-dashboard/SKILL.md +138 -0
- package/.claude/skills/add-dashboard/resources/dashboard-pusher.ts +495 -0
- package/.claude/skills/add-emacs/SKILL.md +296 -0
- package/.claude/skills/add-gcal-tool/SKILL.md +210 -0
- package/.claude/skills/add-gchat/REMOVE.md +6 -0
- package/.claude/skills/add-gchat/SKILL.md +92 -0
- package/.claude/skills/add-gchat/VERIFY.md +3 -0
- package/.claude/skills/add-github/REMOVE.md +6 -0
- package/.claude/skills/add-github/SKILL.md +148 -0
- package/.claude/skills/add-github/VERIFY.md +3 -0
- package/.claude/skills/add-gmail-tool/SKILL.md +229 -0
- package/.claude/skills/add-imessage/REMOVE.md +6 -0
- package/.claude/skills/add-imessage/SKILL.md +113 -0
- package/.claude/skills/add-imessage/VERIFY.md +3 -0
- package/.claude/skills/add-karpathy-llm-wiki/SKILL.md +110 -0
- package/.claude/skills/add-karpathy-llm-wiki/llm-wiki.md +75 -0
- package/.claude/skills/add-linear/REMOVE.md +6 -0
- package/.claude/skills/add-linear/SKILL.md +168 -0
- package/.claude/skills/add-linear/VERIFY.md +3 -0
- package/.claude/skills/add-macos-statusbar/SKILL.md +133 -0
- package/.claude/skills/add-macos-statusbar/add/src/statusbar.swift +147 -0
- package/.claude/skills/add-matrix/REMOVE.md +6 -0
- package/.claude/skills/add-matrix/SKILL.md +148 -0
- package/.claude/skills/add-matrix/VERIFY.md +3 -0
- package/.claude/skills/add-ollama-provider/SKILL.md +179 -0
- package/.claude/skills/add-ollama-tool/SKILL.md +193 -0
- package/.claude/skills/add-opencode/SKILL.md +229 -0
- package/.claude/skills/add-parallel/SKILL.md +290 -0
- package/.claude/skills/add-resend/REMOVE.md +6 -0
- package/.claude/skills/add-resend/SKILL.md +93 -0
- package/.claude/skills/add-resend/VERIFY.md +3 -0
- package/.claude/skills/add-signal/REMOVE.md +13 -0
- package/.claude/skills/add-signal/SKILL.md +318 -0
- package/.claude/skills/add-signal/VERIFY.md +5 -0
- package/.claude/skills/add-slack/REMOVE.md +6 -0
- package/.claude/skills/add-slack/SKILL.md +112 -0
- package/.claude/skills/add-slack/VERIFY.md +3 -0
- package/.claude/skills/add-teams/REMOVE.md +6 -0
- package/.claude/skills/add-teams/SKILL.md +207 -0
- package/.claude/skills/add-teams/VERIFY.md +3 -0
- package/.claude/skills/add-vercel/SKILL.md +147 -0
- package/.claude/skills/add-vercel/container-skills/vercel-cli/SKILL.md +103 -0
- package/.claude/skills/add-webex/REMOVE.md +6 -0
- package/.claude/skills/add-webex/SKILL.md +88 -0
- package/.claude/skills/add-webex/VERIFY.md +3 -0
- package/.claude/skills/add-wechat/REMOVE.md +49 -0
- package/.claude/skills/add-wechat/SKILL.md +170 -0
- package/.claude/skills/add-wechat/scripts/wire-dm.ts +172 -0
- package/.claude/skills/add-whatsapp/SKILL.md +264 -0
- package/.claude/skills/add-whatsapp-cloud/REMOVE.md +6 -0
- package/.claude/skills/add-whatsapp-cloud/SKILL.md +95 -0
- package/.claude/skills/add-whatsapp-cloud/VERIFY.md +3 -0
- package/.claude/skills/claw/SKILL.md +131 -0
- package/.claude/skills/claw/scripts/claw +374 -0
- package/.claude/skills/convert-to-apple-container/SKILL.md +212 -0
- package/.claude/skills/customize/SKILL.md +110 -0
- package/.claude/skills/debug/SKILL.md +349 -0
- package/.claude/skills/get-qodo-rules/SKILL.md +122 -0
- package/.claude/skills/get-qodo-rules/references/output-format.md +41 -0
- package/.claude/skills/get-qodo-rules/references/pagination.md +33 -0
- package/.claude/skills/get-qodo-rules/references/repository-scope.md +26 -0
- package/.claude/skills/init-first-agent/SKILL.md +120 -0
- package/.claude/skills/init-onecli/SKILL.md +270 -0
- package/.claude/skills/manage-channels/SKILL.md +87 -0
- package/.claude/skills/manage-mounts/SKILL.md +47 -0
- package/.claude/skills/migrate-from-openclaw/MIGRATE_CRONS.md +100 -0
- package/.claude/skills/migrate-from-openclaw/SKILL.md +447 -0
- package/.claude/skills/migrate-from-openclaw/scripts/discover-openclaw.ts +734 -0
- package/.claude/skills/migrate-from-openclaw/scripts/extract-channel-credentials.ts +476 -0
- package/.claude/skills/migrate-nanoclaw/SKILL.md +484 -0
- package/.claude/skills/migrate-nanoclaw/diagnostics.md +51 -0
- package/.claude/skills/qodo-pr-resolver/SKILL.md +326 -0
- package/.claude/skills/qodo-pr-resolver/resources/providers.md +329 -0
- package/.claude/skills/update-nanoclaw/SKILL.md +243 -0
- package/.claude/skills/update-nanoclaw/diagnostics.md +48 -0
- package/.claude/skills/update-skills/SKILL.md +130 -0
- package/.claude/skills/use-native-credential-proxy/SKILL.md +167 -0
- package/.claude/skills/x-integration/SKILL.md +417 -0
- package/.claude/skills/x-integration/agent.ts +243 -0
- package/.claude/skills/x-integration/host.ts +155 -0
- package/.claude/skills/x-integration/lib/browser.ts +148 -0
- package/.claude/skills/x-integration/lib/config.ts +62 -0
- package/.claude/skills/x-integration/scripts/like.ts +56 -0
- package/.claude/skills/x-integration/scripts/post.ts +66 -0
- package/.claude/skills/x-integration/scripts/quote.ts +80 -0
- package/.claude/skills/x-integration/scripts/reply.ts +74 -0
- package/.claude/skills/x-integration/scripts/retweet.ts +62 -0
- package/.claude/skills/x-integration/scripts/setup.ts +87 -0
- package/.github/CODEOWNERS +10 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +18 -0
- package/.github/workflows/bump-version.yml +35 -0
- package/.github/workflows/ci.yml +39 -0
- package/.github/workflows/label-pr.yml +40 -0
- package/.github/workflows/update-tokens.yml +43 -0
- package/.husky/pre-commit +1 -0
- package/.mcp.json +3 -0
- package/.nvmrc +1 -0
- package/.parachute/module.json +14 -0
- package/.prettierrc +4 -0
- package/CHANGELOG.md +215 -0
- package/CLAUDE.md +307 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +159 -0
- package/CONTRIBUTORS.md +26 -0
- package/LICENSE +21 -0
- package/README.md +190 -0
- package/README_ja.md +194 -0
- package/README_zh.md +194 -0
- package/assets/nanoclaw-favicon.png +0 -0
- package/assets/nanoclaw-icon.png +0 -0
- package/assets/nanoclaw-logo-dark.png +0 -0
- package/assets/nanoclaw-logo.png +0 -0
- package/assets/nanoclaw-profile.jpeg +0 -0
- package/assets/nanoclaw-sales.png +0 -0
- package/assets/social-preview.jpg +0 -0
- package/config-examples/mount-allowlist.json +25 -0
- package/container/.dockerignore +2 -0
- package/container/CLAUDE.md +21 -0
- package/container/Dockerfile +121 -0
- package/container/agent-runner/bun.lock +243 -0
- package/container/agent-runner/package.json +22 -0
- package/container/agent-runner/scripts/sdk-signal-probe.ts +169 -0
- package/container/agent-runner/src/config.ts +55 -0
- package/container/agent-runner/src/db/connection.ts +267 -0
- package/container/agent-runner/src/db/index.ts +20 -0
- package/container/agent-runner/src/db/messages-in.ts +138 -0
- package/container/agent-runner/src/db/messages-out.ts +143 -0
- package/container/agent-runner/src/db/session-routing.ts +30 -0
- package/container/agent-runner/src/db/session-state.test.ts +100 -0
- package/container/agent-runner/src/db/session-state.ts +79 -0
- package/container/agent-runner/src/destinations.ts +135 -0
- package/container/agent-runner/src/formatter.test.ts +167 -0
- package/container/agent-runner/src/formatter.ts +260 -0
- package/container/agent-runner/src/index.ts +110 -0
- package/container/agent-runner/src/integration.test.ts +121 -0
- package/container/agent-runner/src/mcp-tools/agents.instructions.md +26 -0
- package/container/agent-runner/src/mcp-tools/agents.ts +66 -0
- package/container/agent-runner/src/mcp-tools/core.instructions.md +27 -0
- package/container/agent-runner/src/mcp-tools/core.ts +262 -0
- package/container/agent-runner/src/mcp-tools/index.ts +22 -0
- package/container/agent-runner/src/mcp-tools/interactive.instructions.md +22 -0
- package/container/agent-runner/src/mcp-tools/interactive.ts +169 -0
- package/container/agent-runner/src/mcp-tools/scheduling.instructions.md +40 -0
- package/container/agent-runner/src/mcp-tools/scheduling.ts +299 -0
- package/container/agent-runner/src/mcp-tools/self-mod.instructions.md +25 -0
- package/container/agent-runner/src/mcp-tools/self-mod.ts +120 -0
- package/container/agent-runner/src/mcp-tools/server.ts +54 -0
- package/container/agent-runner/src/mcp-tools/types.ts +6 -0
- package/container/agent-runner/src/poll-loop.test.ts +248 -0
- package/container/agent-runner/src/poll-loop.ts +437 -0
- package/container/agent-runner/src/providers/claude.ts +379 -0
- package/container/agent-runner/src/providers/factory.test.ts +19 -0
- package/container/agent-runner/src/providers/factory.ts +13 -0
- package/container/agent-runner/src/providers/index.ts +6 -0
- package/container/agent-runner/src/providers/mock.ts +77 -0
- package/container/agent-runner/src/providers/provider-registry.ts +33 -0
- package/container/agent-runner/src/providers/types.ts +82 -0
- package/container/agent-runner/src/scheduling/task-script.ts +121 -0
- package/container/agent-runner/src/timezone.test.ts +93 -0
- package/container/agent-runner/src/timezone.ts +107 -0
- package/container/agent-runner/tsconfig.json +14 -0
- package/container/build.sh +48 -0
- package/container/entrypoint.sh +16 -0
- package/container/skills/agent-browser/SKILL.md +159 -0
- package/container/skills/frontend-engineer/SKILL.md +157 -0
- package/container/skills/self-customize/SKILL.md +87 -0
- package/container/skills/slack-formatting/SKILL.md +94 -0
- package/container/skills/vercel-cli/SKILL.md +111 -0
- package/container/skills/welcome/SKILL.md +85 -0
- package/docs/APPLE-CONTAINER-NETWORKING.md +90 -0
- package/docs/BRANCH-FORK-MAINTENANCE.md +81 -0
- package/docs/README.md +25 -0
- package/docs/SDK_DEEP_DIVE.md +643 -0
- package/docs/SECURITY.md +162 -0
- package/docs/agent-runner-details.md +749 -0
- package/docs/api-details.md +365 -0
- package/docs/architecture-diagram.html +422 -0
- package/docs/architecture-diagram.md +215 -0
- package/docs/architecture.md +751 -0
- package/docs/audit/2026-04-30-channel-endpoint-audit.md +36 -0
- package/docs/build-and-runtime.md +80 -0
- package/docs/cross-mount-stress/README.md +112 -0
- package/docs/cross-mount-stress/container-writer-retry.mjs +55 -0
- package/docs/cross-mount-stress/container-writer-slow.mjs +42 -0
- package/docs/cross-mount-stress/container-writer.mjs +47 -0
- package/docs/cross-mount-stress/host-writer-retry.mjs +55 -0
- package/docs/cross-mount-stress/host-writer-slow.mjs +43 -0
- package/docs/cross-mount-stress/host-writer.mjs +47 -0
- package/docs/db-central.md +316 -0
- package/docs/db-session.md +183 -0
- package/docs/db.md +119 -0
- package/docs/design/2026-04-29-vault-management-ui.md +231 -0
- package/docs/design/2026-04-30-channel-wiring-rework.md +234 -0
- package/docs/design/2026-05-01-channel-wiring-approvals-deep-dive.md +272 -0
- package/docs/design/2026-05-02-channel-policy-and-approval-routing.md +250 -0
- package/docs/docker-sandboxes.md +359 -0
- package/docs/isolation-model.md +88 -0
- package/docs/ollama.md +79 -0
- package/docs/parachute-integration.md +109 -0
- package/docs/post-night-rebirth-reflections.md +151 -0
- package/eslint.config.js +32 -0
- package/package.json +54 -0
- package/pnpm-workspace.yaml +8 -0
- package/repo-tokens/README.md +113 -0
- package/repo-tokens/action.yml +186 -0
- package/repo-tokens/badge.svg +23 -0
- package/repo-tokens/examples/green.svg +14 -0
- package/repo-tokens/examples/red.svg +14 -0
- package/repo-tokens/examples/yellow-green.svg +14 -0
- package/repo-tokens/examples/yellow.svg +14 -0
- package/scripts/chat.ts +101 -0
- package/scripts/cleanup-sessions.sh +150 -0
- package/scripts/init-cli-agent.ts +171 -0
- package/scripts/init-first-agent.ts +377 -0
- package/scripts/parachute.ts +158 -0
- package/scripts/run-migrations.ts +105 -0
- package/scripts/sanity-live-poll.ts +95 -0
- package/scripts/seed-discord.ts +79 -0
- package/scripts/test-v2-agent.ts +106 -0
- package/scripts/test-v2-channel-e2e.ts +265 -0
- package/scripts/test-v2-host.ts +184 -0
- package/src/channels/adapter.ts +214 -0
- package/src/channels/ask-question.ts +46 -0
- package/src/channels/channel-registry.test.ts +421 -0
- package/src/channels/channel-registry.ts +313 -0
- package/src/channels/chat-sdk-bridge.test.ts +84 -0
- package/src/channels/chat-sdk-bridge.ts +652 -0
- package/src/channels/cli.ts +276 -0
- package/src/channels/discord.ts +90 -0
- package/src/channels/index.ts +17 -0
- package/src/channels/telegram-markdown-sanitize.test.ts +78 -0
- package/src/channels/telegram-markdown-sanitize.ts +55 -0
- package/src/channels/telegram-pairing.test.ts +254 -0
- package/src/channels/telegram-pairing.ts +339 -0
- package/src/channels/telegram.ts +279 -0
- package/src/channels/trust-hint.test.ts +48 -0
- package/src/channels/trust-hint.ts +75 -0
- package/src/claude-md-compose.migrate.test.ts +64 -0
- package/src/claude-md-compose.ts +205 -0
- package/src/command-gate.ts +63 -0
- package/src/config.test.ts +93 -0
- package/src/config.ts +108 -0
- package/src/container-config.ts +167 -0
- package/src/container-runner.test.ts +32 -0
- package/src/container-runner.ts +576 -0
- package/src/container-runtime.test.ts +169 -0
- package/src/container-runtime.ts +92 -0
- package/src/db/_bun-sqlite-shim.ts +88 -0
- package/src/db/agent-activity.test.ts +155 -0
- package/src/db/agent-activity.ts +121 -0
- package/src/db/agent-groups.ts +77 -0
- package/src/db/connection.migrate.test.ts +143 -0
- package/src/db/connection.ts +224 -0
- package/src/db/db-v2.test.ts +440 -0
- package/src/db/dropped-messages.ts +44 -0
- package/src/db/index.ts +40 -0
- package/src/db/messaging-groups.ts +252 -0
- package/src/db/migrations/001-initial.ts +112 -0
- package/src/db/migrations/002-chat-sdk-state.ts +36 -0
- package/src/db/migrations/008-dropped-messages.ts +27 -0
- package/src/db/migrations/009-drop-pending-credentials.ts +13 -0
- package/src/db/migrations/010-engage-modes.ts +103 -0
- package/src/db/migrations/011-pending-sender-approvals.ts +40 -0
- package/src/db/migrations/012-channel-registration.ts +48 -0
- package/src/db/migrations/013-approval-render-metadata.ts +27 -0
- package/src/db/migrations/014-secrets.ts +44 -0
- package/src/db/migrations/015-secrets-drop-host-pattern.ts +18 -0
- package/src/db/migrations/016-secret-assignments.ts +30 -0
- package/src/db/migrations/017-agent-activity.ts +40 -0
- package/src/db/migrations/018-oauth-app-configs.ts +34 -0
- package/src/db/migrations/019-oauth-app-connections.ts +48 -0
- package/src/db/migrations/020-agent-app-connections.ts +28 -0
- package/src/db/migrations/021-pending-oauth-states.ts +35 -0
- package/src/db/migrations/022-app-connections-provider.ts +25 -0
- package/src/db/migrations/023-agent-group-secret-mode.test.ts +124 -0
- package/src/db/migrations/023-agent-group-secret-mode.ts +65 -0
- package/src/db/migrations/024-collapse-approvals.test.ts +249 -0
- package/src/db/migrations/024-collapse-approvals.ts +182 -0
- package/src/db/migrations/025-secret-mode-check.test.ts +155 -0
- package/src/db/migrations/025-secret-mode-check.ts +49 -0
- package/src/db/migrations/026-user-dms-bot-id.test.ts +116 -0
- package/src/db/migrations/026-user-dms-bot-id.ts +54 -0
- package/src/db/migrations/027-provider-credentials.ts +41 -0
- package/src/db/migrations/_test-helpers.ts +41 -0
- package/src/db/migrations/index.ts +127 -0
- package/src/db/migrations/module-agent-to-agent-destinations.ts +84 -0
- package/src/db/migrations/module-approvals-pending-approvals.ts +42 -0
- package/src/db/migrations/module-approvals-title-options.ts +40 -0
- package/src/db/schema.ts +258 -0
- package/src/db/session-db.test.ts +93 -0
- package/src/db/session-db.ts +325 -0
- package/src/db/sessions.ts +241 -0
- package/src/delivery.test.ts +148 -0
- package/src/delivery.ts +445 -0
- package/src/env.ts +74 -0
- package/src/group-folder.test.ts +35 -0
- package/src/group-folder.ts +44 -0
- package/src/group-init.ts +92 -0
- package/src/host-core.test.ts +456 -0
- package/src/host-sweep.test.ts +146 -0
- package/src/host-sweep.ts +287 -0
- package/src/index.ts +227 -0
- package/src/install-slug.ts +33 -0
- package/src/log.test.ts +81 -0
- package/src/log.ts +117 -0
- package/src/mcp/http.ts +72 -0
- package/src/mcp/server.ts +92 -0
- package/src/mcp/stdio.ts +51 -0
- package/src/mcp/tools/activity.ts +88 -0
- package/src/mcp/tools/agent-groups.ts +183 -0
- package/src/mcp/tools/approvals.ts +122 -0
- package/src/mcp/tools/channels.ts +199 -0
- package/src/mcp/tools/index.ts +27 -0
- package/src/mcp/tools/oauth.ts +48 -0
- package/src/mcp/tools/secrets.ts +169 -0
- package/src/mcp/tools/sessions.ts +135 -0
- package/src/mcp/types.ts +51 -0
- package/src/modules/agent-to-agent/agent-route.test.ts +46 -0
- package/src/modules/agent-to-agent/agent-route.ts +223 -0
- package/src/modules/agent-to-agent/create-agent.ts +127 -0
- package/src/modules/agent-to-agent/db/agent-destinations.ts +135 -0
- package/src/modules/agent-to-agent/index.ts +22 -0
- package/src/modules/agent-to-agent/write-destinations.ts +59 -0
- package/src/modules/approvals/agent.md +45 -0
- package/src/modules/approvals/index.ts +21 -0
- package/src/modules/approvals/picks.test.ts +291 -0
- package/src/modules/approvals/primitive.ts +279 -0
- package/src/modules/approvals/project.md +27 -0
- package/src/modules/approvals/response-handler.ts +87 -0
- package/src/modules/index.ts +24 -0
- package/src/modules/interactive/agent.md +21 -0
- package/src/modules/interactive/index.ts +69 -0
- package/src/modules/interactive/project.md +12 -0
- package/src/modules/mount-security/index.ts +448 -0
- package/src/modules/mount-security/migrate.test.ts +91 -0
- package/src/modules/permissions/access.ts +28 -0
- package/src/modules/permissions/channel-approval.test.ts +389 -0
- package/src/modules/permissions/channel-approval.ts +188 -0
- package/src/modules/permissions/db/agent-group-members.ts +44 -0
- package/src/modules/permissions/db/pending-channel-approvals.test.ts +86 -0
- package/src/modules/permissions/db/pending-channel-approvals.ts +66 -0
- package/src/modules/permissions/db/pending-sender-approvals.ts +60 -0
- package/src/modules/permissions/db/user-dms.ts +58 -0
- package/src/modules/permissions/db/user-roles.ts +85 -0
- package/src/modules/permissions/db/users.ts +38 -0
- package/src/modules/permissions/index.ts +421 -0
- package/src/modules/permissions/permissions.test.ts +358 -0
- package/src/modules/permissions/sender-approval.test.ts +470 -0
- package/src/modules/permissions/sender-approval.ts +165 -0
- package/src/modules/permissions/user-dm.ts +200 -0
- package/src/modules/provider-credentials/db.ts +121 -0
- package/src/modules/provider-credentials/index.ts +12 -0
- package/src/modules/provider-credentials/spawn.test.ts +206 -0
- package/src/modules/provider-credentials/spawn.ts +114 -0
- package/src/modules/scheduling/actions.ts +113 -0
- package/src/modules/scheduling/db.test.ts +282 -0
- package/src/modules/scheduling/db.ts +148 -0
- package/src/modules/scheduling/index.ts +34 -0
- package/src/modules/scheduling/recurrence.test.ts +98 -0
- package/src/modules/scheduling/recurrence.ts +54 -0
- package/src/modules/self-mod/agent.md +30 -0
- package/src/modules/self-mod/apply.ts +85 -0
- package/src/modules/self-mod/index.ts +30 -0
- package/src/modules/self-mod/project.md +39 -0
- package/src/modules/self-mod/request.ts +91 -0
- package/src/modules/typing/index.ts +165 -0
- package/src/oauth/agent-app-connections.ts +103 -0
- package/src/oauth/app-configs.test.ts +64 -0
- package/src/oauth/app-configs.ts +114 -0
- package/src/oauth/app-connections.test.ts +109 -0
- package/src/oauth/app-connections.ts +178 -0
- package/src/oauth/crypto.ts +56 -0
- package/src/oauth/flow.ts +104 -0
- package/src/oauth/providers/google.test.ts +38 -0
- package/src/oauth/providers/google.ts +46 -0
- package/src/oauth/providers/index.ts +48 -0
- package/src/oauth/state-store.test.ts +54 -0
- package/src/oauth/state-store.ts +93 -0
- package/src/parachute/README.md +27 -0
- package/src/parachute/create-agent.test.ts +83 -0
- package/src/parachute/create-agent.ts +122 -0
- package/src/parachute/group-status.test.ts +165 -0
- package/src/parachute/group-status.ts +136 -0
- package/src/parachute/types.ts +41 -0
- package/src/parachute/vault-mcp.test.ts +251 -0
- package/src/parachute/vault-mcp.ts +232 -0
- package/src/platform-id.test.ts +104 -0
- package/src/platform-id.ts +109 -0
- package/src/providers/index.ts +6 -0
- package/src/providers/provider-container-registry.ts +58 -0
- package/src/response-registry.ts +45 -0
- package/src/router.ts +530 -0
- package/src/secrets/crypto.test.ts +45 -0
- package/src/secrets/crypto.ts +55 -0
- package/src/secrets/index.ts +355 -0
- package/src/secrets/master-key.ts +70 -0
- package/src/secrets/secrets.test.ts +354 -0
- package/src/session-manager.migrate.test.ts +59 -0
- package/src/session-manager.ts +433 -0
- package/src/startup-bootstrap.test.ts +226 -0
- package/src/startup-bootstrap.ts +207 -0
- package/src/state-sqlite.ts +182 -0
- package/src/timezone.test.ts +64 -0
- package/src/timezone.ts +37 -0
- package/src/types.ts +230 -0
- package/src/web/auth.test.ts +335 -0
- package/src/web/auth.ts +214 -0
- package/src/web/discord-validate.test.ts +77 -0
- package/src/web/discord-validate.ts +88 -0
- package/src/web/hub-discovery.test.ts +98 -0
- package/src/web/hub-discovery.ts +69 -0
- package/src/web/routes/activity.ts +106 -0
- package/src/web/routes/agent-provider.test.ts +282 -0
- package/src/web/routes/agent-provider.ts +309 -0
- package/src/web/routes/approvals.ts +185 -0
- package/src/web/routes/apps.ts +434 -0
- package/src/web/routes/channels-mg-detail.test.ts +324 -0
- package/src/web/routes/channels-mga-detail.test.ts +425 -0
- package/src/web/routes/channels.ts +489 -0
- package/src/web/routes/oauth-providers.ts +42 -0
- package/src/web/routes/secrets.test.ts +175 -0
- package/src/web/routes/secrets.ts +282 -0
- package/src/web/routes/sessions.ts +123 -0
- package/src/web/routes/settings.test.ts +106 -0
- package/src/web/routes/settings.ts +247 -0
- package/src/web/routes/setup-status.ts +205 -0
- package/src/web/routes/vaults.test.ts +389 -0
- package/src/web/routes/vaults.ts +225 -0
- package/src/web/server-version.test.ts +16 -0
- package/src/web/server.ts +1003 -0
- package/src/web/services-manifest.test.ts +120 -0
- package/src/web/services-manifest.ts +61 -0
- package/src/web/static-serve.test.ts +255 -0
- package/src/web/static-serve.ts +104 -0
- package/src/web/telegram-validate.test.ts +116 -0
- package/src/web/telegram-validate.ts +107 -0
- package/src/web/vault-proxy.test.ts +214 -0
- package/src/web/vault-proxy.ts +120 -0
- package/src/web/wire-channel.ts +181 -0
- package/src/webhook-server.ts +134 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +18 -0
- package/web/README.md +63 -0
- package/web/ui/index.html +13 -0
- package/web/ui/package.json +35 -0
- package/web/ui/pnpm-lock.yaml +2164 -0
- package/web/ui/scripts/verify-base.mjs +31 -0
- package/web/ui/src/App.tsx +88 -0
- package/web/ui/src/components/ActivityFeed.tsx +444 -0
- package/web/ui/src/components/AgentGroupPicker.tsx +263 -0
- package/web/ui/src/components/AgentProviderCards.tsx +220 -0
- package/web/ui/src/components/CredentialForm.tsx +214 -0
- package/web/ui/src/components/ScopeGrants.tsx +74 -0
- package/web/ui/src/components/StatusDot.tsx +43 -0
- package/web/ui/src/components/VaultPicker.tsx +127 -0
- package/web/ui/src/components/setup/AdapterInstallStep.tsx +178 -0
- package/web/ui/src/components/setup/AgentGroupStep.tsx +43 -0
- package/web/ui/src/components/setup/ChannelPickStep.tsx +74 -0
- package/web/ui/src/components/setup/DoneStep.tsx +49 -0
- package/web/ui/src/components/setup/PrereqStep.tsx +129 -0
- package/web/ui/src/components/setup/TestConnectionStep.tsx +108 -0
- package/web/ui/src/components/setup/TestMessageStep.tsx +104 -0
- package/web/ui/src/components/setup/WireChannelStep.tsx +166 -0
- package/web/ui/src/components/setup/types.ts +105 -0
- package/web/ui/src/lib/api.test.ts +410 -0
- package/web/ui/src/lib/api.ts +1210 -0
- package/web/ui/src/lib/auth.test.ts +139 -0
- package/web/ui/src/lib/auth.ts +348 -0
- package/web/ui/src/lib/channel-adapters.ts +136 -0
- package/web/ui/src/main.tsx +19 -0
- package/web/ui/src/routes/ApprovalsList.tsx +294 -0
- package/web/ui/src/routes/Apps.tsx +613 -0
- package/web/ui/src/routes/ChannelWireDetail.test.tsx +233 -0
- package/web/ui/src/routes/ChannelWireDetail.tsx +403 -0
- package/web/ui/src/routes/ChannelsList.tsx +158 -0
- package/web/ui/src/routes/GroupDetail.tsx +755 -0
- package/web/ui/src/routes/GroupList.tsx +187 -0
- package/web/ui/src/routes/MessagingGroupDetail.test.tsx +233 -0
- package/web/ui/src/routes/MessagingGroupDetail.tsx +306 -0
- package/web/ui/src/routes/NewGroupWizard.tsx +390 -0
- package/web/ui/src/routes/OAuthCallback.tsx +56 -0
- package/web/ui/src/routes/SecretsList.tsx +921 -0
- package/web/ui/src/routes/SessionsList.tsx +220 -0
- package/web/ui/src/routes/SettingsAgentProvider.tsx +109 -0
- package/web/ui/src/routes/SettingsApprovals.tsx +234 -0
- package/web/ui/src/routes/SetupWizard.tsx +219 -0
- package/web/ui/src/routes/VaultDetail.test.tsx +361 -0
- package/web/ui/src/routes/VaultDetail.tsx +960 -0
- package/web/ui/src/routes/VaultsList.tsx +295 -0
- package/web/ui/src/routes/WireChannelPage.tsx +413 -0
- package/web/ui/src/styles.css +608 -0
- package/web/ui/src/test/setup.ts +23 -0
- package/web/ui/src/vite-env.d.ts +10 -0
- package/web/ui/tsconfig.json +20 -0
- package/web/ui/vite.config.ts +34 -0
- package/web/ui/vitest.config.ts +25 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock log
|
|
4
|
+
vi.mock('./log.js', () => ({
|
|
5
|
+
log: {
|
|
6
|
+
debug: vi.fn(),
|
|
7
|
+
info: vi.fn(),
|
|
8
|
+
warn: vi.fn(),
|
|
9
|
+
error: vi.fn(),
|
|
10
|
+
fatal: vi.fn(),
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
// Mock child_process — store the mock fn so tests can configure it
|
|
15
|
+
const mockExecSync = vi.fn();
|
|
16
|
+
vi.mock('child_process', () => ({
|
|
17
|
+
execSync: (...args: unknown[]) => mockExecSync(...args),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
CONTAINER_RUNTIME_BIN,
|
|
22
|
+
readonlyMountArgs,
|
|
23
|
+
stopContainer,
|
|
24
|
+
ensureContainerRuntimeRunning,
|
|
25
|
+
cleanupOrphans,
|
|
26
|
+
} from './container-runtime.js';
|
|
27
|
+
import { CONTAINER_INSTALL_LABEL, LEGACY_PARACLAW_INSTALL_LABEL } from './config.js';
|
|
28
|
+
import { log } from './log.js';
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.clearAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// --- Pure functions ---
|
|
35
|
+
|
|
36
|
+
describe('readonlyMountArgs', () => {
|
|
37
|
+
it('returns -v flag with :ro suffix', () => {
|
|
38
|
+
const args = readonlyMountArgs('/host/path', '/container/path');
|
|
39
|
+
expect(args).toEqual(['-v', '/host/path:/container/path:ro']);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('stopContainer', () => {
|
|
44
|
+
it('calls docker stop for valid container names', () => {
|
|
45
|
+
stopContainer('parachute-agent-test-123');
|
|
46
|
+
expect(mockExecSync).toHaveBeenCalledWith(`${CONTAINER_RUNTIME_BIN} stop -t 1 parachute-agent-test-123`, {
|
|
47
|
+
stdio: 'pipe',
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('rejects names with shell metacharacters', () => {
|
|
52
|
+
expect(() => stopContainer('foo; rm -rf /')).toThrow('Invalid container name');
|
|
53
|
+
expect(() => stopContainer('foo$(whoami)')).toThrow('Invalid container name');
|
|
54
|
+
expect(() => stopContainer('foo`id`')).toThrow('Invalid container name');
|
|
55
|
+
expect(mockExecSync).not.toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// --- ensureContainerRuntimeRunning ---
|
|
60
|
+
|
|
61
|
+
describe('ensureContainerRuntimeRunning', () => {
|
|
62
|
+
it('does nothing when runtime is already running', () => {
|
|
63
|
+
mockExecSync.mockReturnValueOnce('');
|
|
64
|
+
|
|
65
|
+
ensureContainerRuntimeRunning();
|
|
66
|
+
|
|
67
|
+
expect(mockExecSync).toHaveBeenCalledTimes(1);
|
|
68
|
+
expect(mockExecSync).toHaveBeenCalledWith(`${CONTAINER_RUNTIME_BIN} info`, {
|
|
69
|
+
stdio: 'pipe',
|
|
70
|
+
timeout: 10000,
|
|
71
|
+
});
|
|
72
|
+
expect(log.debug).toHaveBeenCalledWith('Container runtime already running');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('throws when docker info fails', () => {
|
|
76
|
+
mockExecSync.mockImplementationOnce(() => {
|
|
77
|
+
throw new Error('Cannot connect to the Docker daemon');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(() => ensureContainerRuntimeRunning()).toThrow('Container runtime is required but failed to start');
|
|
81
|
+
expect(log.error).toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// --- cleanupOrphans ---
|
|
86
|
+
|
|
87
|
+
describe('cleanupOrphans', () => {
|
|
88
|
+
it('filters ps by both the new and legacy install labels so peers are not reaped', () => {
|
|
89
|
+
mockExecSync.mockReturnValue('');
|
|
90
|
+
|
|
91
|
+
cleanupOrphans();
|
|
92
|
+
|
|
93
|
+
expect(mockExecSync).toHaveBeenNthCalledWith(
|
|
94
|
+
1,
|
|
95
|
+
`${CONTAINER_RUNTIME_BIN} ps --filter label=${CONTAINER_INSTALL_LABEL} --format '{{.Names}}'`,
|
|
96
|
+
expect.any(Object),
|
|
97
|
+
);
|
|
98
|
+
expect(mockExecSync).toHaveBeenNthCalledWith(
|
|
99
|
+
2,
|
|
100
|
+
`${CONTAINER_RUNTIME_BIN} ps --filter label=${LEGACY_PARACLAW_INSTALL_LABEL} --format '{{.Names}}'`,
|
|
101
|
+
expect.any(Object),
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('stops orphaned containers from both labels and de-dupes', () => {
|
|
106
|
+
// First ps (new label) returns one container; second ps (legacy label) returns two —
|
|
107
|
+
// one duplicates the first, simulating a container that carries both labels during
|
|
108
|
+
// upgrade.
|
|
109
|
+
mockExecSync.mockReturnValueOnce('parachute-agent-group1-111\n');
|
|
110
|
+
mockExecSync.mockReturnValueOnce('parachute-agent-group1-111\nparaclaw-group2-222\n');
|
|
111
|
+
mockExecSync.mockReturnValue('');
|
|
112
|
+
|
|
113
|
+
cleanupOrphans();
|
|
114
|
+
|
|
115
|
+
// 2 ps + 2 unique stop calls
|
|
116
|
+
expect(mockExecSync).toHaveBeenCalledTimes(4);
|
|
117
|
+
expect(mockExecSync).toHaveBeenNthCalledWith(3, `${CONTAINER_RUNTIME_BIN} stop -t 1 parachute-agent-group1-111`, {
|
|
118
|
+
stdio: 'pipe',
|
|
119
|
+
});
|
|
120
|
+
expect(mockExecSync).toHaveBeenNthCalledWith(4, `${CONTAINER_RUNTIME_BIN} stop -t 1 paraclaw-group2-222`, {
|
|
121
|
+
stdio: 'pipe',
|
|
122
|
+
});
|
|
123
|
+
expect(log.info).toHaveBeenCalledWith('Stopped orphaned containers', {
|
|
124
|
+
count: 2,
|
|
125
|
+
names: ['parachute-agent-group1-111', 'paraclaw-group2-222'],
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('does nothing when no orphans exist on either label', () => {
|
|
130
|
+
mockExecSync.mockReturnValue('');
|
|
131
|
+
|
|
132
|
+
cleanupOrphans();
|
|
133
|
+
|
|
134
|
+
expect(mockExecSync).toHaveBeenCalledTimes(2); // both label queries
|
|
135
|
+
expect(log.info).not.toHaveBeenCalled();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('warns and continues when ps fails', () => {
|
|
139
|
+
mockExecSync.mockImplementationOnce(() => {
|
|
140
|
+
throw new Error('docker not available');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
cleanupOrphans(); // should not throw
|
|
144
|
+
|
|
145
|
+
expect(log.warn).toHaveBeenCalledWith(
|
|
146
|
+
'Failed to clean up orphaned containers',
|
|
147
|
+
expect.objectContaining({ err: expect.any(Error) }),
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('continues stopping remaining containers when one stop fails', () => {
|
|
152
|
+
mockExecSync.mockReturnValueOnce('parachute-agent-a-1\nparachute-agent-b-2\n');
|
|
153
|
+
mockExecSync.mockReturnValueOnce(''); // legacy label query empty
|
|
154
|
+
// First stop fails
|
|
155
|
+
mockExecSync.mockImplementationOnce(() => {
|
|
156
|
+
throw new Error('already stopped');
|
|
157
|
+
});
|
|
158
|
+
// Second stop succeeds
|
|
159
|
+
mockExecSync.mockReturnValueOnce('');
|
|
160
|
+
|
|
161
|
+
cleanupOrphans(); // should not throw
|
|
162
|
+
|
|
163
|
+
expect(mockExecSync).toHaveBeenCalledTimes(4);
|
|
164
|
+
expect(log.info).toHaveBeenCalledWith('Stopped orphaned containers', {
|
|
165
|
+
count: 2,
|
|
166
|
+
names: ['parachute-agent-a-1', 'parachute-agent-b-2'],
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Container runtime abstraction for parachute-agent.
|
|
3
|
+
* All runtime-specific logic lives here so swapping runtimes means changing one file.
|
|
4
|
+
*/
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
|
|
8
|
+
import { CONTAINER_INSTALL_LABEL, LEGACY_PARACLAW_INSTALL_LABEL } from './config.js';
|
|
9
|
+
import { log } from './log.js';
|
|
10
|
+
|
|
11
|
+
/** The container runtime binary name. */
|
|
12
|
+
export const CONTAINER_RUNTIME_BIN = 'docker';
|
|
13
|
+
|
|
14
|
+
/** CLI args needed for the container to resolve the host gateway. */
|
|
15
|
+
export function hostGatewayArgs(): string[] {
|
|
16
|
+
// On Linux, host.docker.internal isn't built-in — add it explicitly
|
|
17
|
+
if (os.platform() === 'linux') {
|
|
18
|
+
return ['--add-host=host.docker.internal:host-gateway'];
|
|
19
|
+
}
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Returns CLI args for a readonly bind mount. */
|
|
24
|
+
export function readonlyMountArgs(hostPath: string, containerPath: string): string[] {
|
|
25
|
+
return ['-v', `${hostPath}:${containerPath}:ro`];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Stop a container by name. Uses execFileSync to avoid shell injection. */
|
|
29
|
+
export function stopContainer(name: string): void {
|
|
30
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(name)) {
|
|
31
|
+
throw new Error(`Invalid container name: ${name}`);
|
|
32
|
+
}
|
|
33
|
+
execSync(`${CONTAINER_RUNTIME_BIN} stop -t 1 ${name}`, { stdio: 'pipe' });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Ensure the container runtime is running, starting it if needed. */
|
|
37
|
+
export function ensureContainerRuntimeRunning(): void {
|
|
38
|
+
try {
|
|
39
|
+
execSync(`${CONTAINER_RUNTIME_BIN} info`, {
|
|
40
|
+
stdio: 'pipe',
|
|
41
|
+
timeout: 10000,
|
|
42
|
+
});
|
|
43
|
+
log.debug('Container runtime already running');
|
|
44
|
+
} catch (err) {
|
|
45
|
+
log.error('Failed to reach container runtime', { err });
|
|
46
|
+
console.error('\n╔════════════════════════════════════════════════════════════════╗');
|
|
47
|
+
console.error('║ FATAL: Container runtime failed to start ║');
|
|
48
|
+
console.error('║ ║');
|
|
49
|
+
console.error('║ Agents cannot run without a container runtime. To fix: ║');
|
|
50
|
+
console.error('║ 1. Ensure Docker is installed and running ║');
|
|
51
|
+
console.error('║ 2. Run: docker info ║');
|
|
52
|
+
console.error('║ 3. Restart parachute-agent ║');
|
|
53
|
+
console.error('╚════════════════════════════════════════════════════════════════╝\n');
|
|
54
|
+
throw new Error('Container runtime is required but failed to start', {
|
|
55
|
+
cause: err,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Kill orphaned parachute-agent containers from THIS install's previous runs.
|
|
62
|
+
*
|
|
63
|
+
* Scoped by the `parachute-agent-install=<slug>` label (and the pre-0.1.0
|
|
64
|
+
* `paraclaw-install=<slug>` label for one upgrade cycle) so a crash-looping
|
|
65
|
+
* peer install cannot reap our containers, and we cannot reap theirs. The
|
|
66
|
+
* label is stamped onto every container at spawn time — see
|
|
67
|
+
* container-runner.ts. Old-label compat reap is queued to drop in 0.2.0.
|
|
68
|
+
*/
|
|
69
|
+
export function cleanupOrphans(): void {
|
|
70
|
+
try {
|
|
71
|
+
const namesByLabel = [CONTAINER_INSTALL_LABEL, LEGACY_PARACLAW_INSTALL_LABEL].flatMap((label) => {
|
|
72
|
+
const output = execSync(`${CONTAINER_RUNTIME_BIN} ps --filter label=${label} --format '{{.Names}}'`, {
|
|
73
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
74
|
+
encoding: 'utf-8',
|
|
75
|
+
});
|
|
76
|
+
return output.trim().split('\n').filter(Boolean);
|
|
77
|
+
});
|
|
78
|
+
const orphans = Array.from(new Set(namesByLabel));
|
|
79
|
+
for (const name of orphans) {
|
|
80
|
+
try {
|
|
81
|
+
stopContainer(name);
|
|
82
|
+
} catch {
|
|
83
|
+
/* already stopped */
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (orphans.length > 0) {
|
|
87
|
+
log.info('Stopped orphaned containers', { count: orphans.length, names: orphans });
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
log.warn('Failed to clean up orphaned containers', { err });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test-only shim that re-exports the `bun:sqlite` surface backed by
|
|
3
|
+
* `better-sqlite3`. Wired in via vitest's `resolve.alias` so the host code
|
|
4
|
+
* (which imports `bun:sqlite`) can be exercised under Node + vitest without
|
|
5
|
+
* a real bun runtime.
|
|
6
|
+
*
|
|
7
|
+
* Production code runs under Bun and sees the real `bun:sqlite` module —
|
|
8
|
+
* never this shim. Symmetric inverse of the wrapper in connection.ts:
|
|
9
|
+
* connection.ts prefixes object keys with `@` for bun's binder; this shim
|
|
10
|
+
* strips that prefix so better-sqlite3's binder is happy.
|
|
11
|
+
*/
|
|
12
|
+
import BetterSqlite3 from 'better-sqlite3';
|
|
13
|
+
|
|
14
|
+
type Bindable = unknown;
|
|
15
|
+
|
|
16
|
+
function stripPrefix(key: string): string {
|
|
17
|
+
return key.startsWith('@') || key.startsWith('$') || key.startsWith(':') ? key.slice(1) : key;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function adaptArgs(args: Bindable[]): Bindable[] {
|
|
21
|
+
return args.map((a) => {
|
|
22
|
+
if (a == null) return a;
|
|
23
|
+
if (Array.isArray(a)) return a;
|
|
24
|
+
if (typeof a !== 'object') return a;
|
|
25
|
+
const out: Record<string, unknown> = {};
|
|
26
|
+
for (const [k, v] of Object.entries(a as Record<string, unknown>)) {
|
|
27
|
+
out[stripPrefix(k)] = v;
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class ShimStatement {
|
|
34
|
+
constructor(private readonly stmt: BetterSqlite3.Statement) {}
|
|
35
|
+
run(...args: Bindable[]): { changes: number; lastInsertRowid: number | bigint } {
|
|
36
|
+
const r = this.stmt.run(...(adaptArgs(args) as never[]));
|
|
37
|
+
return { changes: r.changes, lastInsertRowid: r.lastInsertRowid };
|
|
38
|
+
}
|
|
39
|
+
get<T = unknown>(...args: Bindable[]): T | null {
|
|
40
|
+
const r = this.stmt.get(...(adaptArgs(args) as never[]));
|
|
41
|
+
return (r ?? null) as T | null;
|
|
42
|
+
}
|
|
43
|
+
all<T = unknown>(...args: Bindable[]): T[] {
|
|
44
|
+
return this.stmt.all(...(adaptArgs(args) as never[])) as T[];
|
|
45
|
+
}
|
|
46
|
+
values(...args: Bindable[]): unknown[][] {
|
|
47
|
+
return this.stmt.raw().all(...(adaptArgs(args) as never[])) as unknown[][];
|
|
48
|
+
}
|
|
49
|
+
iterate<T = unknown>(...args: Bindable[]): IterableIterator<T> {
|
|
50
|
+
return this.stmt.iterate(...(adaptArgs(args) as never[])) as IterableIterator<T>;
|
|
51
|
+
}
|
|
52
|
+
finalize(): void {
|
|
53
|
+
/* no-op — better-sqlite3 finalizes on GC */
|
|
54
|
+
}
|
|
55
|
+
toString(): string {
|
|
56
|
+
return this.stmt.source;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class Database {
|
|
61
|
+
public readonly raw: BetterSqlite3.Database;
|
|
62
|
+
constructor(path: string, opts?: { readonly?: boolean }) {
|
|
63
|
+
this.raw = new BetterSqlite3(path, opts);
|
|
64
|
+
}
|
|
65
|
+
prepare(sql: string): ShimStatement {
|
|
66
|
+
return new ShimStatement(this.raw.prepare(sql));
|
|
67
|
+
}
|
|
68
|
+
exec(sql: string): void {
|
|
69
|
+
this.raw.exec(sql);
|
|
70
|
+
}
|
|
71
|
+
query(sql: string): ShimStatement {
|
|
72
|
+
return this.prepare(sql);
|
|
73
|
+
}
|
|
74
|
+
run(sql: string): void {
|
|
75
|
+
this.raw.exec(sql);
|
|
76
|
+
}
|
|
77
|
+
transaction<F extends (...a: never[]) => unknown>(fn: F): F {
|
|
78
|
+
return this.raw.transaction(fn) as unknown as F;
|
|
79
|
+
}
|
|
80
|
+
close(): void {
|
|
81
|
+
this.raw.close();
|
|
82
|
+
}
|
|
83
|
+
get filename(): string {
|
|
84
|
+
return this.raw.name;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type Statement = ShimStatement;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createAgentGroup, createSession, closeDb, getDb, initTestDb, runMigrations } from './index.js';
|
|
4
|
+
import {
|
|
5
|
+
getActivitySyncedSeq,
|
|
6
|
+
listActivityByAgentGroup,
|
|
7
|
+
listActivityBySession,
|
|
8
|
+
mergeActivityBatch,
|
|
9
|
+
} from './agent-activity.js';
|
|
10
|
+
import type { OutboundActivityRow } from './session-db.js';
|
|
11
|
+
|
|
12
|
+
function now(): string {
|
|
13
|
+
return new Date().toISOString();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function seedAgentAndSession(): { agentGroupId: string; sessionId: string } {
|
|
17
|
+
createAgentGroup({
|
|
18
|
+
id: 'ag-1',
|
|
19
|
+
name: 'Test Agent',
|
|
20
|
+
folder: 'test-agent',
|
|
21
|
+
agent_provider: null,
|
|
22
|
+
created_at: now(),
|
|
23
|
+
});
|
|
24
|
+
createSession({
|
|
25
|
+
id: 'sess-1',
|
|
26
|
+
agent_group_id: 'ag-1',
|
|
27
|
+
messaging_group_id: null,
|
|
28
|
+
thread_id: null,
|
|
29
|
+
agent_provider: null,
|
|
30
|
+
status: 'active',
|
|
31
|
+
container_status: 'running',
|
|
32
|
+
last_active: now(),
|
|
33
|
+
created_at: now(),
|
|
34
|
+
});
|
|
35
|
+
return { agentGroupId: 'ag-1', sessionId: 'sess-1' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function row(seq: number, kind: string, target: string | null, summary: string | null = null): OutboundActivityRow {
|
|
39
|
+
return { seq, ts: new Date(2026, 0, seq).toISOString(), kind, target, summary };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
const db = initTestDb();
|
|
44
|
+
runMigrations(db);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
closeDb();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('agent_activity merge', () => {
|
|
52
|
+
it('initial cursor is 0 and merging a batch advances it', () => {
|
|
53
|
+
const { agentGroupId, sessionId } = seedAgentAndSession();
|
|
54
|
+
expect(getActivitySyncedSeq(sessionId)).toBe(0);
|
|
55
|
+
|
|
56
|
+
const newCursor = mergeActivityBatch(agentGroupId, sessionId, [
|
|
57
|
+
row(1, 'tool_call', 'Read'),
|
|
58
|
+
row(2, 'cmd_exec', 'Bash'),
|
|
59
|
+
row(3, 'mcp_call', 'mcp__parachute_agent__schedule_task'),
|
|
60
|
+
]);
|
|
61
|
+
expect(newCursor).toBe(3);
|
|
62
|
+
expect(getActivitySyncedSeq(sessionId)).toBe(3);
|
|
63
|
+
|
|
64
|
+
const rows = listActivityBySession(sessionId);
|
|
65
|
+
expect(rows).toHaveLength(3);
|
|
66
|
+
expect(rows.map((r) => r.kind).sort()).toEqual(['cmd_exec', 'mcp_call', 'tool_call']);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('empty batch is a no-op and returns the existing cursor', () => {
|
|
70
|
+
const { agentGroupId, sessionId } = seedAgentAndSession();
|
|
71
|
+
mergeActivityBatch(agentGroupId, sessionId, [row(1, 'tool_call', 'Read')]);
|
|
72
|
+
expect(getActivitySyncedSeq(sessionId)).toBe(1);
|
|
73
|
+
|
|
74
|
+
const c = mergeActivityBatch(agentGroupId, sessionId, []);
|
|
75
|
+
expect(c).toBe(1);
|
|
76
|
+
expect(listActivityBySession(sessionId)).toHaveLength(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('cursor advance is monotonic — re-merging an older batch leaves it unchanged', () => {
|
|
80
|
+
const { agentGroupId, sessionId } = seedAgentAndSession();
|
|
81
|
+
mergeActivityBatch(agentGroupId, sessionId, [row(5, 'tool_call', 'Read'), row(6, 'tool_call', 'Glob')]);
|
|
82
|
+
expect(getActivitySyncedSeq(sessionId)).toBe(6);
|
|
83
|
+
|
|
84
|
+
// The delivery loop guards against this with `seq > cursor`, but if a
|
|
85
|
+
// caller passes older rows, the cursor should NOT regress.
|
|
86
|
+
mergeActivityBatch(agentGroupId, sessionId, [row(2, 'tool_call', 'Read')]);
|
|
87
|
+
expect(getActivitySyncedSeq(sessionId)).toBe(6);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('listActivityByAgentGroup returns rows for a single group, newest first', () => {
|
|
91
|
+
const { agentGroupId, sessionId } = seedAgentAndSession();
|
|
92
|
+
createAgentGroup({
|
|
93
|
+
id: 'ag-2',
|
|
94
|
+
name: 'Other',
|
|
95
|
+
folder: 'other',
|
|
96
|
+
agent_provider: null,
|
|
97
|
+
created_at: now(),
|
|
98
|
+
});
|
|
99
|
+
createSession({
|
|
100
|
+
id: 'sess-2',
|
|
101
|
+
agent_group_id: 'ag-2',
|
|
102
|
+
messaging_group_id: null,
|
|
103
|
+
thread_id: null,
|
|
104
|
+
agent_provider: null,
|
|
105
|
+
status: 'active',
|
|
106
|
+
container_status: 'running',
|
|
107
|
+
last_active: now(),
|
|
108
|
+
created_at: now(),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
mergeActivityBatch(agentGroupId, sessionId, [row(1, 'tool_call', 'Read'), row(2, 'cmd_exec', 'Bash')]);
|
|
112
|
+
mergeActivityBatch('ag-2', 'sess-2', [row(1, 'tool_call', 'Glob')]);
|
|
113
|
+
|
|
114
|
+
const ag1 = listActivityByAgentGroup(agentGroupId);
|
|
115
|
+
expect(ag1).toHaveLength(2);
|
|
116
|
+
expect(ag1.every((r) => r.agent_group_id === agentGroupId)).toBe(true);
|
|
117
|
+
// DESC by created_at — row(2) was minted later (Jan 2 > Jan 1).
|
|
118
|
+
expect(ag1[0].target).toBe('Bash');
|
|
119
|
+
expect(ag1[1].target).toBe('Read');
|
|
120
|
+
|
|
121
|
+
const ag2 = listActivityByAgentGroup('ag-2');
|
|
122
|
+
expect(ag2).toHaveLength(1);
|
|
123
|
+
expect(ag2[0].target).toBe('Glob');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('honors `since` and `limit`', () => {
|
|
127
|
+
const { agentGroupId, sessionId } = seedAgentAndSession();
|
|
128
|
+
mergeActivityBatch(agentGroupId, sessionId, [
|
|
129
|
+
row(1, 'tool_call', 'A'),
|
|
130
|
+
row(2, 'tool_call', 'B'),
|
|
131
|
+
row(3, 'tool_call', 'C'),
|
|
132
|
+
row(4, 'tool_call', 'D'),
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
const limited = listActivityBySession(sessionId, { limit: 2 });
|
|
136
|
+
expect(limited).toHaveLength(2);
|
|
137
|
+
expect(limited[0].target).toBe('D'); // newest first
|
|
138
|
+
expect(limited[1].target).toBe('C');
|
|
139
|
+
|
|
140
|
+
// since = ts of seq=2 (Jan 2). Should include C (Jan 3) and D (Jan 4).
|
|
141
|
+
const since = new Date(2026, 0, 2).toISOString();
|
|
142
|
+
const sinceRows = listActivityBySession(sessionId, { since });
|
|
143
|
+
expect(sinceRows.map((r) => r.target)).toEqual(['D', 'C']);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('cascades on session delete', () => {
|
|
147
|
+
const { agentGroupId, sessionId } = seedAgentAndSession();
|
|
148
|
+
mergeActivityBatch(agentGroupId, sessionId, [row(1, 'tool_call', 'Read')]);
|
|
149
|
+
expect(listActivityBySession(sessionId)).toHaveLength(1);
|
|
150
|
+
|
|
151
|
+
getDb().prepare('DELETE FROM sessions WHERE id = ?').run(sessionId);
|
|
152
|
+
expect(listActivityBySession(sessionId)).toHaveLength(0);
|
|
153
|
+
expect(listActivityByAgentGroup(agentGroupId)).toHaveLength(0);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central `agent_activity` ledger — append-only, drained from each session's
|
|
3
|
+
* outbound.db `activity` table during delivery. Read paths feed the web UI's
|
|
4
|
+
* "what is this agent doing" surface; the table is intended to be
|
|
5
|
+
* inexpensively scannable per-agent-group and per-session, hence the two
|
|
6
|
+
* descending indexes added in migration 017.
|
|
7
|
+
*
|
|
8
|
+
* Cursor: `sessions.activity_synced_seq` is the per-session high-water mark
|
|
9
|
+
* — every row from outbound.db with `seq <= cursor` has either been merged
|
|
10
|
+
* here or was emitted by a session whose container has since stopped (we
|
|
11
|
+
* still drained it on the way out). The merge is single-writer because the
|
|
12
|
+
* delivery loop already serializes per-session via `inflightDeliveries`.
|
|
13
|
+
*/
|
|
14
|
+
import crypto from 'node:crypto';
|
|
15
|
+
|
|
16
|
+
import { getDb } from './connection.js';
|
|
17
|
+
import type { OutboundActivityRow } from './session-db.js';
|
|
18
|
+
|
|
19
|
+
export type ActivityKind = 'tool_call' | 'mcp_call' | 'cmd_exec' | 'secret_use';
|
|
20
|
+
|
|
21
|
+
export interface ActivityRow {
|
|
22
|
+
id: string;
|
|
23
|
+
agent_group_id: string;
|
|
24
|
+
session_id: string;
|
|
25
|
+
kind: string;
|
|
26
|
+
target: string | null;
|
|
27
|
+
summary: string | null;
|
|
28
|
+
created_at: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getActivitySyncedSeq(sessionId: string): number {
|
|
32
|
+
const row = getDb().prepare('SELECT activity_synced_seq AS seq FROM sessions WHERE id = ?').get(sessionId) as
|
|
33
|
+
| { seq: number }
|
|
34
|
+
| undefined;
|
|
35
|
+
return row?.seq ?? 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Insert a batch of outbound activity rows into central `agent_activity` and
|
|
40
|
+
* advance the session's merge cursor in one transaction. No-op when the
|
|
41
|
+
* input is empty so callers don't have to short-circuit. Returns the new
|
|
42
|
+
* cursor value (max input seq, or the unchanged prior cursor).
|
|
43
|
+
*/
|
|
44
|
+
export function mergeActivityBatch(agentGroupId: string, sessionId: string, rows: OutboundActivityRow[]): number {
|
|
45
|
+
if (rows.length === 0) return getActivitySyncedSeq(sessionId);
|
|
46
|
+
|
|
47
|
+
const db = getDb();
|
|
48
|
+
const insert = db.prepare(
|
|
49
|
+
`INSERT INTO agent_activity (id, agent_group_id, session_id, kind, target, summary, created_at)
|
|
50
|
+
VALUES (@id, @agent_group_id, @session_id, @kind, @target, @summary, @created_at)`,
|
|
51
|
+
);
|
|
52
|
+
const updateCursor = db.prepare(
|
|
53
|
+
`UPDATE sessions SET activity_synced_seq = ? WHERE id = ? AND activity_synced_seq < ?`,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const maxSeq = rows.reduce((acc, r) => (r.seq > acc ? r.seq : acc), 0);
|
|
57
|
+
|
|
58
|
+
db.transaction(() => {
|
|
59
|
+
for (const r of rows) {
|
|
60
|
+
insert.run({
|
|
61
|
+
id: crypto.randomUUID(),
|
|
62
|
+
agent_group_id: agentGroupId,
|
|
63
|
+
session_id: sessionId,
|
|
64
|
+
kind: r.kind,
|
|
65
|
+
target: r.target,
|
|
66
|
+
summary: r.summary,
|
|
67
|
+
created_at: r.ts,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
updateCursor.run(maxSeq, sessionId, maxSeq);
|
|
71
|
+
})();
|
|
72
|
+
|
|
73
|
+
return maxSeq;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ListActivityOpts {
|
|
77
|
+
/** Only return rows with created_at > since (ISO 8601). */
|
|
78
|
+
since?: string;
|
|
79
|
+
/** Cap row count. Defaults to 100, hard-capped at 500 by the route layer. */
|
|
80
|
+
limit?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function listActivityByAgentGroup(agentGroupId: string, opts: ListActivityOpts = {}): ActivityRow[] {
|
|
84
|
+
const limit = opts.limit ?? 100;
|
|
85
|
+
if (opts.since) {
|
|
86
|
+
return getDb()
|
|
87
|
+
.prepare(
|
|
88
|
+
`SELECT * FROM agent_activity
|
|
89
|
+
WHERE agent_group_id = ? AND created_at > ?
|
|
90
|
+
ORDER BY created_at DESC LIMIT ?`,
|
|
91
|
+
)
|
|
92
|
+
.all(agentGroupId, opts.since, limit) as ActivityRow[];
|
|
93
|
+
}
|
|
94
|
+
return getDb()
|
|
95
|
+
.prepare(
|
|
96
|
+
`SELECT * FROM agent_activity
|
|
97
|
+
WHERE agent_group_id = ?
|
|
98
|
+
ORDER BY created_at DESC LIMIT ?`,
|
|
99
|
+
)
|
|
100
|
+
.all(agentGroupId, limit) as ActivityRow[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function listActivityBySession(sessionId: string, opts: ListActivityOpts = {}): ActivityRow[] {
|
|
104
|
+
const limit = opts.limit ?? 100;
|
|
105
|
+
if (opts.since) {
|
|
106
|
+
return getDb()
|
|
107
|
+
.prepare(
|
|
108
|
+
`SELECT * FROM agent_activity
|
|
109
|
+
WHERE session_id = ? AND created_at > ?
|
|
110
|
+
ORDER BY created_at DESC LIMIT ?`,
|
|
111
|
+
)
|
|
112
|
+
.all(sessionId, opts.since, limit) as ActivityRow[];
|
|
113
|
+
}
|
|
114
|
+
return getDb()
|
|
115
|
+
.prepare(
|
|
116
|
+
`SELECT * FROM agent_activity
|
|
117
|
+
WHERE session_id = ?
|
|
118
|
+
ORDER BY created_at DESC LIMIT ?`,
|
|
119
|
+
)
|
|
120
|
+
.all(sessionId, limit) as ActivityRow[];
|
|
121
|
+
}
|