@openparachute/agent 0.1.2 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.parachute/module.json +124 -8
- package/LICENSE +2 -16
- package/README.md +118 -166
- package/package.json +35 -42
- package/scripts/spawn-agent.ts +371 -0
- package/src/_parked/interactive-spawn.test.ts +324 -0
- package/src/_parked/interactive-spawn.ts +701 -0
- package/src/agent-defs.test.ts +1504 -0
- package/src/agent-defs.ts +1702 -0
- package/src/agent-mcp-config.test.ts +115 -0
- package/src/agent-mcp-config.ts +115 -0
- package/src/agents.test.ts +360 -0
- package/src/agents.ts +379 -0
- package/src/auth.test.ts +46 -0
- package/src/auth.ts +140 -0
- package/src/backends/attached-queue.test.ts +376 -0
- package/src/backends/attached-queue.ts +372 -0
- package/src/backends/programmatic.test.ts +1715 -0
- package/src/backends/programmatic.ts +927 -0
- package/src/backends/registry.test.ts +1494 -0
- package/src/backends/registry.ts +1202 -0
- package/src/backends/stream-json.test.ts +570 -0
- package/src/backends/stream-json.ts +392 -0
- package/src/backends/types.ts +223 -0
- package/src/bridge.ts +417 -0
- package/src/channel-backend-wiring.test.ts +237 -0
- package/src/credentials.test.ts +274 -0
- package/src/credentials.ts +380 -0
- package/src/cron.test.ts +342 -0
- package/src/cron.ts +380 -0
- package/src/daemon-agent-def-api.test.ts +166 -0
- package/src/daemon-agent-defs-api.test.ts +953 -0
- package/src/daemon-agent-env-api.test.ts +338 -0
- package/src/daemon-attached-queue-store.test.ts +65 -0
- package/src/daemon-config-api.test.ts +962 -0
- package/src/daemon-jobs-api.test.ts +271 -0
- package/src/daemon-vault-chat.test.ts +250 -0
- package/src/daemon.test.ts +746 -0
- package/src/daemon.ts +3314 -0
- package/src/def-vaults.test.ts +136 -0
- package/src/def-vaults.ts +165 -0
- package/src/delivery-state.test.ts +110 -0
- package/src/delivery-state.ts +154 -0
- package/src/effective-env.test.ts +114 -0
- package/src/effective-env.ts +184 -0
- package/src/env-compat.ts +39 -0
- package/src/grants.test.ts +638 -0
- package/src/grants.ts +675 -0
- package/src/hub-jwt.test.ts +161 -0
- package/src/hub-jwt.ts +182 -0
- package/src/jobs.test.ts +245 -0
- package/src/jobs.ts +266 -0
- package/src/mcp-http.test.ts +265 -0
- package/src/mcp-http.ts +771 -0
- package/src/mint-token.test.ts +152 -0
- package/src/mint-token.ts +139 -0
- package/src/module-manifest.test.ts +158 -0
- package/src/oauth-discovery.ts +134 -0
- package/src/programmatic-wiring.test.ts +838 -0
- package/src/registry.test.ts +227 -0
- package/src/registry.ts +228 -0
- package/src/resolve-port.test.ts +64 -0
- package/src/routing.test.ts +184 -0
- package/src/routing.ts +76 -0
- package/src/runner.test.ts +506 -0
- package/src/runner.ts +255 -0
- package/src/sandbox/config.test.ts +150 -0
- package/src/sandbox/config.ts +102 -0
- package/src/sandbox/egress.test.ts +113 -0
- package/src/sandbox/egress.ts +123 -0
- package/src/sandbox/index.ts +180 -0
- package/src/sandbox/live-seatbelt.test.ts +277 -0
- package/src/sandbox/mounts.test.ts +154 -0
- package/src/sandbox/mounts.ts +133 -0
- package/src/sandbox/sandbox.test.ts +168 -0
- package/src/sandbox/types.ts +382 -0
- package/src/services-manifest.test.ts +106 -0
- package/src/services-manifest.ts +95 -0
- package/src/spa-serve.test.ts +116 -0
- package/src/spa-serve.ts +116 -0
- package/src/spawn-agent-cli.test.ts +172 -0
- package/src/spawn-agent.test.ts +1218 -0
- package/src/spawn-agent.ts +569 -0
- package/src/spawn-deps.test.ts +54 -0
- package/src/spawn-deps.ts +166 -0
- package/src/telegram/api.ts +153 -0
- package/src/terminal-assets.test.ts +50 -0
- package/src/terminal-assets.ts +79 -0
- package/src/terminal-ui.ts +305 -0
- package/src/terminal.test.ts +530 -0
- package/src/terminal.ts +458 -0
- package/src/transport.ts +270 -0
- package/src/transports/http-ui.test.ts +455 -0
- package/src/transports/http-ui.ts +201 -0
- package/src/transports/telegram.test.ts +174 -0
- package/src/transports/telegram.ts +426 -0
- package/src/transports/vault.test.ts +2011 -0
- package/src/transports/vault.ts +1790 -0
- package/src/ui-kit.test.ts +178 -0
- package/src/ui-kit.ts +402 -0
- package/tsconfig.json +8 -14
- package/web/ui/dist/assets/index-C-iWdFFV.css +1 -0
- package/web/ui/dist/assets/index-VFETBk0a.js +60 -0
- package/web/ui/dist/index.html +15 -0
- package/web/ui/tsconfig.json +2 -1
- package/.claude/scheduled_tasks.lock +0 -1
- package/.claude/settings.json +0 -5
- package/.claude/skills/add-atomic-chat-tool/SKILL.md +0 -243
- package/.claude/skills/add-atomic-chat-tool/atomic-chat-mcp-stdio.ts +0 -229
- package/.claude/skills/add-codex/SKILL.md +0 -161
- package/.claude/skills/add-dashboard/SKILL.md +0 -138
- package/.claude/skills/add-dashboard/resources/dashboard-pusher.ts +0 -495
- package/.claude/skills/add-emacs/SKILL.md +0 -296
- package/.claude/skills/add-gcal-tool/SKILL.md +0 -210
- package/.claude/skills/add-gchat/REMOVE.md +0 -6
- package/.claude/skills/add-gchat/SKILL.md +0 -92
- package/.claude/skills/add-gchat/VERIFY.md +0 -3
- package/.claude/skills/add-github/REMOVE.md +0 -6
- package/.claude/skills/add-github/SKILL.md +0 -148
- package/.claude/skills/add-github/VERIFY.md +0 -3
- package/.claude/skills/add-gmail-tool/SKILL.md +0 -229
- package/.claude/skills/add-imessage/REMOVE.md +0 -6
- package/.claude/skills/add-imessage/SKILL.md +0 -113
- package/.claude/skills/add-imessage/VERIFY.md +0 -3
- package/.claude/skills/add-karpathy-llm-wiki/SKILL.md +0 -110
- package/.claude/skills/add-karpathy-llm-wiki/llm-wiki.md +0 -75
- package/.claude/skills/add-linear/REMOVE.md +0 -6
- package/.claude/skills/add-linear/SKILL.md +0 -168
- package/.claude/skills/add-linear/VERIFY.md +0 -3
- package/.claude/skills/add-macos-statusbar/SKILL.md +0 -133
- package/.claude/skills/add-macos-statusbar/add/src/statusbar.swift +0 -147
- package/.claude/skills/add-matrix/REMOVE.md +0 -6
- package/.claude/skills/add-matrix/SKILL.md +0 -148
- package/.claude/skills/add-matrix/VERIFY.md +0 -3
- package/.claude/skills/add-ollama-provider/SKILL.md +0 -179
- package/.claude/skills/add-ollama-tool/SKILL.md +0 -193
- package/.claude/skills/add-opencode/SKILL.md +0 -229
- package/.claude/skills/add-parallel/SKILL.md +0 -290
- package/.claude/skills/add-resend/REMOVE.md +0 -6
- package/.claude/skills/add-resend/SKILL.md +0 -93
- package/.claude/skills/add-resend/VERIFY.md +0 -3
- package/.claude/skills/add-signal/REMOVE.md +0 -13
- package/.claude/skills/add-signal/SKILL.md +0 -318
- package/.claude/skills/add-signal/VERIFY.md +0 -5
- package/.claude/skills/add-slack/REMOVE.md +0 -6
- package/.claude/skills/add-slack/SKILL.md +0 -112
- package/.claude/skills/add-slack/VERIFY.md +0 -3
- package/.claude/skills/add-teams/REMOVE.md +0 -6
- package/.claude/skills/add-teams/SKILL.md +0 -207
- package/.claude/skills/add-teams/VERIFY.md +0 -3
- package/.claude/skills/add-vercel/SKILL.md +0 -147
- package/.claude/skills/add-vercel/container-skills/vercel-cli/SKILL.md +0 -103
- package/.claude/skills/add-webex/REMOVE.md +0 -6
- package/.claude/skills/add-webex/SKILL.md +0 -88
- package/.claude/skills/add-webex/VERIFY.md +0 -3
- package/.claude/skills/add-wechat/REMOVE.md +0 -49
- package/.claude/skills/add-wechat/SKILL.md +0 -170
- package/.claude/skills/add-wechat/scripts/wire-dm.ts +0 -172
- package/.claude/skills/add-whatsapp/SKILL.md +0 -264
- package/.claude/skills/add-whatsapp-cloud/REMOVE.md +0 -6
- package/.claude/skills/add-whatsapp-cloud/SKILL.md +0 -95
- package/.claude/skills/add-whatsapp-cloud/VERIFY.md +0 -3
- package/.claude/skills/claw/SKILL.md +0 -131
- package/.claude/skills/claw/scripts/claw +0 -374
- package/.claude/skills/convert-to-apple-container/SKILL.md +0 -212
- package/.claude/skills/customize/SKILL.md +0 -110
- package/.claude/skills/debug/SKILL.md +0 -349
- package/.claude/skills/get-qodo-rules/SKILL.md +0 -122
- package/.claude/skills/get-qodo-rules/references/output-format.md +0 -41
- package/.claude/skills/get-qodo-rules/references/pagination.md +0 -33
- package/.claude/skills/get-qodo-rules/references/repository-scope.md +0 -26
- package/.claude/skills/init-first-agent/SKILL.md +0 -120
- package/.claude/skills/init-onecli/SKILL.md +0 -270
- package/.claude/skills/manage-channels/SKILL.md +0 -87
- package/.claude/skills/manage-mounts/SKILL.md +0 -47
- package/.claude/skills/migrate-from-openclaw/MIGRATE_CRONS.md +0 -100
- package/.claude/skills/migrate-from-openclaw/SKILL.md +0 -447
- package/.claude/skills/migrate-from-openclaw/scripts/discover-openclaw.ts +0 -734
- package/.claude/skills/migrate-from-openclaw/scripts/extract-channel-credentials.ts +0 -476
- package/.claude/skills/migrate-nanoclaw/SKILL.md +0 -484
- package/.claude/skills/migrate-nanoclaw/diagnostics.md +0 -51
- package/.claude/skills/qodo-pr-resolver/SKILL.md +0 -326
- package/.claude/skills/qodo-pr-resolver/resources/providers.md +0 -329
- package/.claude/skills/update-nanoclaw/SKILL.md +0 -243
- package/.claude/skills/update-nanoclaw/diagnostics.md +0 -48
- package/.claude/skills/update-skills/SKILL.md +0 -130
- package/.claude/skills/use-native-credential-proxy/SKILL.md +0 -167
- package/.claude/skills/x-integration/SKILL.md +0 -417
- package/.claude/skills/x-integration/agent.ts +0 -243
- package/.claude/skills/x-integration/host.ts +0 -155
- package/.claude/skills/x-integration/lib/browser.ts +0 -148
- package/.claude/skills/x-integration/lib/config.ts +0 -62
- package/.claude/skills/x-integration/scripts/like.ts +0 -56
- package/.claude/skills/x-integration/scripts/post.ts +0 -66
- package/.claude/skills/x-integration/scripts/quote.ts +0 -80
- package/.claude/skills/x-integration/scripts/reply.ts +0 -74
- package/.claude/skills/x-integration/scripts/retweet.ts +0 -62
- package/.claude/skills/x-integration/scripts/setup.ts +0 -87
- package/.github/CODEOWNERS +0 -10
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -18
- package/.github/workflows/bump-version.yml +0 -35
- package/.github/workflows/ci.yml +0 -39
- package/.github/workflows/label-pr.yml +0 -40
- package/.github/workflows/update-tokens.yml +0 -43
- package/.husky/pre-commit +0 -1
- package/.mcp.json +0 -3
- package/.nvmrc +0 -1
- package/.prettierrc +0 -4
- package/CHANGELOG.md +0 -263
- package/CLAUDE.md +0 -307
- package/CODE_OF_CONDUCT.md +0 -128
- package/CONTRIBUTING.md +0 -159
- package/CONTRIBUTORS.md +0 -26
- package/LICENSE-NANOCLAW-MIT +0 -21
- package/README_ja.md +0 -194
- package/README_zh.md +0 -194
- 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 +0 -25
- package/container/.dockerignore +0 -2
- package/container/CLAUDE.md +0 -21
- package/container/Dockerfile +0 -121
- package/container/agent-runner/bun.lock +0 -243
- package/container/agent-runner/package.json +0 -22
- package/container/agent-runner/scripts/sdk-signal-probe.ts +0 -169
- package/container/agent-runner/src/config.ts +0 -55
- package/container/agent-runner/src/db/connection.ts +0 -267
- package/container/agent-runner/src/db/index.ts +0 -20
- package/container/agent-runner/src/db/messages-in.ts +0 -138
- package/container/agent-runner/src/db/messages-out.ts +0 -143
- package/container/agent-runner/src/db/session-routing.ts +0 -30
- package/container/agent-runner/src/db/session-state.test.ts +0 -100
- package/container/agent-runner/src/db/session-state.ts +0 -79
- package/container/agent-runner/src/destinations.ts +0 -135
- package/container/agent-runner/src/formatter.test.ts +0 -167
- package/container/agent-runner/src/formatter.ts +0 -260
- package/container/agent-runner/src/index.ts +0 -110
- package/container/agent-runner/src/integration.test.ts +0 -121
- package/container/agent-runner/src/mcp-tools/agents.instructions.md +0 -26
- package/container/agent-runner/src/mcp-tools/agents.ts +0 -66
- package/container/agent-runner/src/mcp-tools/core.instructions.md +0 -27
- package/container/agent-runner/src/mcp-tools/core.ts +0 -262
- package/container/agent-runner/src/mcp-tools/index.ts +0 -22
- package/container/agent-runner/src/mcp-tools/interactive.instructions.md +0 -22
- package/container/agent-runner/src/mcp-tools/interactive.ts +0 -169
- package/container/agent-runner/src/mcp-tools/scheduling.instructions.md +0 -40
- package/container/agent-runner/src/mcp-tools/scheduling.ts +0 -299
- package/container/agent-runner/src/mcp-tools/self-mod.instructions.md +0 -25
- package/container/agent-runner/src/mcp-tools/self-mod.ts +0 -120
- package/container/agent-runner/src/mcp-tools/server.ts +0 -54
- package/container/agent-runner/src/mcp-tools/types.ts +0 -6
- package/container/agent-runner/src/poll-loop.test.ts +0 -248
- package/container/agent-runner/src/poll-loop.ts +0 -437
- package/container/agent-runner/src/providers/claude.ts +0 -379
- package/container/agent-runner/src/providers/factory.test.ts +0 -19
- package/container/agent-runner/src/providers/factory.ts +0 -13
- package/container/agent-runner/src/providers/index.ts +0 -6
- package/container/agent-runner/src/providers/mock.ts +0 -77
- package/container/agent-runner/src/providers/provider-registry.ts +0 -33
- package/container/agent-runner/src/providers/types.ts +0 -82
- package/container/agent-runner/src/scheduling/task-script.ts +0 -121
- package/container/agent-runner/src/timezone.test.ts +0 -93
- package/container/agent-runner/src/timezone.ts +0 -107
- package/container/agent-runner/tsconfig.json +0 -14
- package/container/build.sh +0 -48
- package/container/entrypoint.sh +0 -16
- package/container/skills/agent-browser/SKILL.md +0 -159
- package/container/skills/frontend-engineer/SKILL.md +0 -157
- package/container/skills/self-customize/SKILL.md +0 -87
- package/container/skills/slack-formatting/SKILL.md +0 -94
- package/container/skills/vercel-cli/SKILL.md +0 -111
- package/container/skills/welcome/SKILL.md +0 -85
- package/docs/APPLE-CONTAINER-NETWORKING.md +0 -90
- package/docs/BRANCH-FORK-MAINTENANCE.md +0 -81
- package/docs/README.md +0 -25
- package/docs/SDK_DEEP_DIVE.md +0 -643
- package/docs/SECURITY.md +0 -162
- package/docs/agent-runner-details.md +0 -749
- package/docs/api-details.md +0 -365
- package/docs/architecture-diagram.html +0 -422
- package/docs/architecture-diagram.md +0 -215
- package/docs/architecture.md +0 -751
- package/docs/audit/2026-04-30-channel-endpoint-audit.md +0 -36
- package/docs/build-and-runtime.md +0 -80
- package/docs/cross-mount-stress/README.md +0 -112
- package/docs/cross-mount-stress/container-writer-retry.mjs +0 -55
- package/docs/cross-mount-stress/container-writer-slow.mjs +0 -42
- package/docs/cross-mount-stress/container-writer.mjs +0 -47
- package/docs/cross-mount-stress/host-writer-retry.mjs +0 -55
- package/docs/cross-mount-stress/host-writer-slow.mjs +0 -43
- package/docs/cross-mount-stress/host-writer.mjs +0 -47
- package/docs/db-central.md +0 -316
- package/docs/db-session.md +0 -183
- package/docs/db.md +0 -119
- package/docs/design/2026-04-29-vault-management-ui.md +0 -231
- package/docs/design/2026-04-30-channel-wiring-rework.md +0 -234
- package/docs/design/2026-05-01-channel-wiring-approvals-deep-dive.md +0 -272
- package/docs/design/2026-05-02-channel-policy-and-approval-routing.md +0 -250
- package/docs/docker-sandboxes.md +0 -359
- package/docs/isolation-model.md +0 -88
- package/docs/ollama.md +0 -79
- package/docs/parachute-integration.md +0 -109
- package/docs/post-night-rebirth-reflections.md +0 -151
- package/eslint.config.js +0 -32
- package/pnpm-workspace.yaml +0 -8
- package/repo-tokens/README.md +0 -113
- package/repo-tokens/action.yml +0 -186
- package/repo-tokens/badge.svg +0 -23
- package/repo-tokens/examples/green.svg +0 -14
- package/repo-tokens/examples/red.svg +0 -14
- package/repo-tokens/examples/yellow-green.svg +0 -14
- package/repo-tokens/examples/yellow.svg +0 -14
- package/scripts/chat.ts +0 -101
- package/scripts/cleanup-sessions.sh +0 -150
- package/scripts/init-cli-agent.ts +0 -172
- package/scripts/init-first-agent.ts +0 -378
- package/scripts/parachute.ts +0 -158
- package/scripts/run-migrations.ts +0 -105
- package/scripts/sanity-live-poll.ts +0 -95
- package/scripts/seed-discord.ts +0 -80
- package/scripts/test-v2-agent.ts +0 -106
- package/scripts/test-v2-channel-e2e.ts +0 -265
- package/scripts/test-v2-host.ts +0 -184
- package/src/channels/adapter.ts +0 -214
- package/src/channels/api-translator.test.ts +0 -306
- package/src/channels/api-translator.ts +0 -214
- package/src/channels/ask-question.ts +0 -46
- package/src/channels/channel-registry.test.ts +0 -421
- package/src/channels/channel-registry.ts +0 -313
- package/src/channels/chat-sdk-bridge.test.ts +0 -84
- package/src/channels/chat-sdk-bridge.ts +0 -652
- package/src/channels/cli.ts +0 -276
- package/src/channels/discord.ts +0 -90
- package/src/channels/index.ts +0 -17
- package/src/channels/telegram-markdown-sanitize.test.ts +0 -78
- package/src/channels/telegram-markdown-sanitize.ts +0 -55
- package/src/channels/telegram-pairing.test.ts +0 -254
- package/src/channels/telegram-pairing.ts +0 -339
- package/src/channels/telegram.ts +0 -279
- package/src/channels/trust-hint.test.ts +0 -48
- package/src/channels/trust-hint.ts +0 -75
- package/src/claude-md-compose.migrate.test.ts +0 -64
- package/src/claude-md-compose.ts +0 -205
- package/src/command-gate.ts +0 -63
- package/src/config.test.ts +0 -93
- package/src/config.ts +0 -128
- package/src/container-config.ts +0 -167
- package/src/container-runner.test.ts +0 -32
- package/src/container-runner.ts +0 -576
- package/src/container-runtime.test.ts +0 -269
- package/src/container-runtime.ts +0 -167
- package/src/db/_bun-sqlite-shim.ts +0 -88
- package/src/db/agent-activity.test.ts +0 -155
- package/src/db/agent-activity.ts +0 -121
- package/src/db/agent-groups.ts +0 -77
- package/src/db/connection.migrate.test.ts +0 -176
- package/src/db/connection.ts +0 -259
- package/src/db/db-v2.test.ts +0 -440
- package/src/db/dropped-messages.ts +0 -44
- package/src/db/index.ts +0 -40
- package/src/db/messaging-groups.ts +0 -252
- package/src/db/migrations/001-initial.ts +0 -112
- package/src/db/migrations/002-chat-sdk-state.ts +0 -36
- package/src/db/migrations/008-dropped-messages.ts +0 -27
- package/src/db/migrations/009-drop-pending-credentials.ts +0 -13
- package/src/db/migrations/010-engage-modes.ts +0 -103
- package/src/db/migrations/011-pending-sender-approvals.ts +0 -40
- package/src/db/migrations/012-channel-registration.ts +0 -48
- package/src/db/migrations/013-approval-render-metadata.ts +0 -27
- package/src/db/migrations/014-secrets.ts +0 -44
- package/src/db/migrations/015-secrets-drop-host-pattern.ts +0 -18
- package/src/db/migrations/016-secret-assignments.ts +0 -30
- package/src/db/migrations/017-agent-activity.ts +0 -40
- package/src/db/migrations/018-oauth-app-configs.ts +0 -34
- package/src/db/migrations/019-oauth-app-connections.ts +0 -48
- package/src/db/migrations/020-agent-app-connections.ts +0 -28
- package/src/db/migrations/021-pending-oauth-states.ts +0 -35
- package/src/db/migrations/022-app-connections-provider.ts +0 -25
- package/src/db/migrations/023-agent-group-secret-mode.test.ts +0 -124
- package/src/db/migrations/023-agent-group-secret-mode.ts +0 -65
- package/src/db/migrations/024-collapse-approvals.test.ts +0 -249
- package/src/db/migrations/024-collapse-approvals.ts +0 -182
- package/src/db/migrations/025-secret-mode-check.test.ts +0 -155
- package/src/db/migrations/025-secret-mode-check.ts +0 -49
- package/src/db/migrations/026-user-dms-bot-id.test.ts +0 -116
- package/src/db/migrations/026-user-dms-bot-id.ts +0 -54
- package/src/db/migrations/027-provider-credentials.ts +0 -41
- package/src/db/migrations/_test-helpers.ts +0 -41
- package/src/db/migrations/index.ts +0 -127
- package/src/db/migrations/module-agent-to-agent-destinations.ts +0 -84
- package/src/db/migrations/module-approvals-pending-approvals.ts +0 -42
- package/src/db/migrations/module-approvals-title-options.ts +0 -40
- package/src/db/schema.ts +0 -258
- package/src/db/session-db.test.ts +0 -93
- package/src/db/session-db.ts +0 -325
- package/src/db/sessions.ts +0 -241
- package/src/delivery.test.ts +0 -148
- package/src/delivery.ts +0 -445
- package/src/env.ts +0 -74
- package/src/group-folder.test.ts +0 -35
- package/src/group-folder.ts +0 -44
- package/src/group-init.ts +0 -92
- package/src/host-core.test.ts +0 -456
- package/src/host-sweep.test.ts +0 -146
- package/src/host-sweep.ts +0 -287
- package/src/index.ts +0 -232
- package/src/install-slug.ts +0 -33
- package/src/log.test.ts +0 -81
- package/src/log.ts +0 -117
- package/src/mcp/http.ts +0 -72
- package/src/mcp/server.ts +0 -92
- package/src/mcp/stdio.ts +0 -51
- package/src/mcp/tools/activity.ts +0 -88
- package/src/mcp/tools/agent-groups.ts +0 -183
- package/src/mcp/tools/approvals.ts +0 -122
- package/src/mcp/tools/channels.test.ts +0 -126
- package/src/mcp/tools/channels.ts +0 -134
- package/src/mcp/tools/index.ts +0 -27
- package/src/mcp/tools/oauth.ts +0 -48
- package/src/mcp/tools/secrets.ts +0 -169
- package/src/mcp/tools/sessions.ts +0 -135
- package/src/mcp/types.ts +0 -51
- package/src/modules/agent-to-agent/agent-route.test.ts +0 -46
- package/src/modules/agent-to-agent/agent-route.ts +0 -223
- package/src/modules/agent-to-agent/create-agent.ts +0 -127
- package/src/modules/agent-to-agent/db/agent-destinations.ts +0 -135
- package/src/modules/agent-to-agent/index.ts +0 -22
- package/src/modules/agent-to-agent/write-destinations.ts +0 -59
- package/src/modules/approvals/agent.md +0 -45
- package/src/modules/approvals/index.ts +0 -21
- package/src/modules/approvals/picks.test.ts +0 -291
- package/src/modules/approvals/primitive.ts +0 -279
- package/src/modules/approvals/project.md +0 -27
- package/src/modules/approvals/response-handler.ts +0 -87
- package/src/modules/index.ts +0 -24
- package/src/modules/interactive/agent.md +0 -21
- package/src/modules/interactive/index.ts +0 -69
- package/src/modules/interactive/project.md +0 -12
- package/src/modules/mount-security/expand-path.test.ts +0 -82
- package/src/modules/mount-security/index.ts +0 -459
- package/src/modules/mount-security/migrate.test.ts +0 -91
- package/src/modules/permissions/access.ts +0 -28
- package/src/modules/permissions/channel-approval.test.ts +0 -389
- package/src/modules/permissions/channel-approval.ts +0 -188
- package/src/modules/permissions/db/agent-group-members.ts +0 -44
- package/src/modules/permissions/db/pending-channel-approvals.test.ts +0 -86
- package/src/modules/permissions/db/pending-channel-approvals.ts +0 -66
- package/src/modules/permissions/db/pending-sender-approvals.ts +0 -60
- package/src/modules/permissions/db/user-dms.ts +0 -58
- package/src/modules/permissions/db/user-roles.ts +0 -85
- package/src/modules/permissions/db/users.ts +0 -38
- package/src/modules/permissions/index.ts +0 -421
- package/src/modules/permissions/permissions.test.ts +0 -358
- package/src/modules/permissions/sender-approval.test.ts +0 -641
- package/src/modules/permissions/sender-approval.ts +0 -165
- package/src/modules/permissions/user-dm.ts +0 -200
- package/src/modules/provider-credentials/db.ts +0 -121
- package/src/modules/provider-credentials/index.ts +0 -12
- package/src/modules/provider-credentials/spawn.test.ts +0 -206
- package/src/modules/provider-credentials/spawn.ts +0 -114
- package/src/modules/scheduling/actions.ts +0 -113
- package/src/modules/scheduling/db.test.ts +0 -282
- package/src/modules/scheduling/db.ts +0 -148
- package/src/modules/scheduling/index.ts +0 -34
- package/src/modules/scheduling/recurrence.test.ts +0 -98
- package/src/modules/scheduling/recurrence.ts +0 -54
- package/src/modules/self-mod/agent.md +0 -30
- package/src/modules/self-mod/apply.ts +0 -85
- package/src/modules/self-mod/index.ts +0 -30
- package/src/modules/self-mod/project.md +0 -39
- package/src/modules/self-mod/request.ts +0 -91
- package/src/modules/typing/index.ts +0 -165
- package/src/oauth/agent-app-connections.ts +0 -103
- package/src/oauth/app-configs.test.ts +0 -64
- package/src/oauth/app-configs.ts +0 -114
- package/src/oauth/app-connections.test.ts +0 -109
- package/src/oauth/app-connections.ts +0 -178
- package/src/oauth/crypto.ts +0 -56
- package/src/oauth/flow.ts +0 -104
- package/src/oauth/providers/google.test.ts +0 -38
- package/src/oauth/providers/google.ts +0 -46
- package/src/oauth/providers/index.ts +0 -48
- package/src/oauth/state-store.test.ts +0 -54
- package/src/oauth/state-store.ts +0 -93
- package/src/parachute/README.md +0 -27
- package/src/parachute/create-agent.test.ts +0 -83
- package/src/parachute/create-agent.ts +0 -122
- package/src/parachute/group-status.test.ts +0 -165
- package/src/parachute/group-status.ts +0 -136
- package/src/parachute/types.ts +0 -41
- package/src/parachute/vault-mcp.test.ts +0 -251
- package/src/parachute/vault-mcp.ts +0 -232
- package/src/platform-id.test.ts +0 -104
- package/src/platform-id.ts +0 -109
- package/src/providers/index.ts +0 -6
- package/src/providers/provider-container-registry.ts +0 -58
- package/src/response-registry.ts +0 -45
- package/src/router.ts +0 -530
- package/src/secrets/crypto.test.ts +0 -45
- package/src/secrets/crypto.ts +0 -55
- package/src/secrets/index.ts +0 -461
- package/src/secrets/master-key.ts +0 -70
- package/src/secrets/secrets.test.ts +0 -651
- package/src/session-manager.attachments.test.ts +0 -171
- package/src/session-manager.dup-skip.test.ts +0 -173
- package/src/session-manager.migrate.test.ts +0 -59
- package/src/session-manager.ts +0 -451
- package/src/startup-bootstrap.test.ts +0 -226
- package/src/startup-bootstrap.ts +0 -207
- package/src/state-sqlite.ts +0 -182
- package/src/timezone.test.ts +0 -64
- package/src/timezone.ts +0 -37
- package/src/types.ts +0 -233
- package/src/web/auth.test.ts +0 -335
- package/src/web/auth.ts +0 -214
- package/src/web/discord-validate.test.ts +0 -77
- package/src/web/discord-validate.ts +0 -88
- package/src/web/hub-discovery.test.ts +0 -98
- package/src/web/hub-discovery.ts +0 -69
- package/src/web/routes/activity.ts +0 -106
- package/src/web/routes/agent-provider.test.ts +0 -282
- package/src/web/routes/agent-provider.ts +0 -309
- package/src/web/routes/approvals.ts +0 -185
- package/src/web/routes/apps.ts +0 -434
- package/src/web/routes/channels-mg-detail.test.ts +0 -324
- package/src/web/routes/channels-mga-detail.test.ts +0 -472
- package/src/web/routes/channels.ts +0 -311
- package/src/web/routes/oauth-providers.ts +0 -42
- package/src/web/routes/secrets.test.ts +0 -220
- package/src/web/routes/secrets.ts +0 -317
- package/src/web/routes/sessions.ts +0 -123
- package/src/web/routes/settings.test.ts +0 -106
- package/src/web/routes/settings.ts +0 -247
- package/src/web/routes/setup-status.ts +0 -205
- package/src/web/routes/vaults.test.ts +0 -389
- package/src/web/routes/vaults.ts +0 -225
- package/src/web/server-version.test.ts +0 -16
- package/src/web/server.ts +0 -1024
- package/src/web/services-manifest.test.ts +0 -148
- package/src/web/services-manifest.ts +0 -66
- package/src/web/static-serve.test.ts +0 -255
- package/src/web/static-serve.ts +0 -104
- package/src/web/telegram-validate.test.ts +0 -116
- package/src/web/telegram-validate.ts +0 -107
- package/src/web/vault-proxy.test.ts +0 -214
- package/src/web/vault-proxy.ts +0 -120
- package/src/web/wire-channel.ts +0 -181
- package/src/webhook-server.ts +0 -134
- package/vitest.config.ts +0 -18
- package/web/README.md +0 -63
- package/web/ui/index.html +0 -13
- package/web/ui/package.json +0 -35
- package/web/ui/pnpm-lock.yaml +0 -2164
- package/web/ui/scripts/verify-base.mjs +0 -31
- package/web/ui/src/App.tsx +0 -88
- package/web/ui/src/components/ActivityFeed.tsx +0 -444
- package/web/ui/src/components/AgentGroupPicker.tsx +0 -263
- package/web/ui/src/components/AgentProviderCards.tsx +0 -220
- package/web/ui/src/components/CredentialForm.tsx +0 -214
- package/web/ui/src/components/ScopeGrants.tsx +0 -74
- package/web/ui/src/components/StatusDot.tsx +0 -43
- package/web/ui/src/components/VaultPicker.tsx +0 -127
- package/web/ui/src/components/setup/AdapterInstallStep.tsx +0 -178
- package/web/ui/src/components/setup/AgentGroupStep.tsx +0 -43
- package/web/ui/src/components/setup/ChannelPickStep.tsx +0 -74
- package/web/ui/src/components/setup/DoneStep.tsx +0 -49
- package/web/ui/src/components/setup/PrereqStep.tsx +0 -129
- package/web/ui/src/components/setup/TestConnectionStep.tsx +0 -108
- package/web/ui/src/components/setup/TestMessageStep.tsx +0 -104
- package/web/ui/src/components/setup/WireChannelStep.tsx +0 -166
- package/web/ui/src/components/setup/types.ts +0 -105
- package/web/ui/src/lib/api.test.ts +0 -410
- package/web/ui/src/lib/api.ts +0 -1248
- package/web/ui/src/lib/auth.test.ts +0 -352
- package/web/ui/src/lib/auth.ts +0 -405
- package/web/ui/src/lib/channel-adapters.ts +0 -136
- package/web/ui/src/main.tsx +0 -19
- package/web/ui/src/routes/ApprovalsList.tsx +0 -294
- package/web/ui/src/routes/Apps.tsx +0 -613
- package/web/ui/src/routes/ChannelWireDetail.test.tsx +0 -233
- package/web/ui/src/routes/ChannelWireDetail.tsx +0 -403
- package/web/ui/src/routes/ChannelsList.tsx +0 -158
- package/web/ui/src/routes/GroupDetail.test.tsx +0 -206
- package/web/ui/src/routes/GroupDetail.tsx +0 -880
- package/web/ui/src/routes/GroupList.tsx +0 -187
- package/web/ui/src/routes/MessagingGroupDetail.test.tsx +0 -233
- package/web/ui/src/routes/MessagingGroupDetail.tsx +0 -306
- package/web/ui/src/routes/NewGroupWizard.tsx +0 -390
- package/web/ui/src/routes/OAuthCallback.tsx +0 -56
- package/web/ui/src/routes/SecretsList.tsx +0 -942
- package/web/ui/src/routes/SessionsList.tsx +0 -220
- package/web/ui/src/routes/SettingsAgentProvider.tsx +0 -109
- package/web/ui/src/routes/SettingsApprovals.tsx +0 -234
- package/web/ui/src/routes/SetupWizard.tsx +0 -219
- package/web/ui/src/routes/VaultDetail.test.tsx +0 -363
- package/web/ui/src/routes/VaultDetail.tsx +0 -960
- package/web/ui/src/routes/VaultsList.tsx +0 -295
- package/web/ui/src/routes/WireChannelPage.tsx +0 -413
- package/web/ui/src/styles.css +0 -608
- package/web/ui/src/test/setup.ts +0 -23
- package/web/ui/src/vite-env.d.ts +0 -10
- package/web/ui/vite.config.ts +0 -34
- package/web/ui/vitest.config.ts +0 -25
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
isAllowedFor,
|
|
4
|
+
chunkText,
|
|
5
|
+
TelegramTransport,
|
|
6
|
+
type AccessConfig,
|
|
7
|
+
} from "./telegram.ts";
|
|
8
|
+
import { ChannelConfigError } from "../transport.ts";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Access control — these cases moved here from the daemon. The policy is now a
|
|
12
|
+
// pure function (isAllowedFor) so it's testable without a live connection.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
function access(partial: Partial<AccessConfig>): AccessConfig {
|
|
16
|
+
return { dmPolicy: "allowlist", allowFrom: [], groups: {}, pending: {}, ...partial };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("isAllowedFor", () => {
|
|
20
|
+
test("open policy allows anyone", () => {
|
|
21
|
+
const a = access({ dmPolicy: "open", allowFrom: [] });
|
|
22
|
+
expect(isAllowedFor(a, 999, 999)).toBe(true);
|
|
23
|
+
expect(isAllowedFor(a, 1, -100)).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("allowlist: user in allowFrom is allowed, others denied", () => {
|
|
27
|
+
const a = access({ allowFrom: ["42"] });
|
|
28
|
+
expect(isAllowedFor(a, 42, 42)).toBe(true);
|
|
29
|
+
expect(isAllowedFor(a, 7, 7)).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("allowInChats group bypass: any member of an allowlisted group gets in", () => {
|
|
33
|
+
const a = access({ allowFrom: ["42"], allowInChats: ["-100200300"] });
|
|
34
|
+
// A user NOT in allowFrom, posting in the allowlisted group → allowed.
|
|
35
|
+
expect(isAllowedFor(a, 999, "-100200300")).toBe(true);
|
|
36
|
+
// Same user in a different group → denied.
|
|
37
|
+
expect(isAllowedFor(a, 999, "-555")).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("allowInChats DM gating: requires BOTH allowFrom AND allowInChats", () => {
|
|
41
|
+
const a = access({ allowFrom: ["42"], allowInChats: ["42"] });
|
|
42
|
+
// user 42 DMing (chat_id === user_id) → both lists include 42 → allowed.
|
|
43
|
+
expect(isAllowedFor(a, 42, 42)).toBe(true);
|
|
44
|
+
// user 42 in a chat NOT in allowInChats → denied.
|
|
45
|
+
expect(isAllowedFor(a, 42, 99)).toBe(false);
|
|
46
|
+
// user not in allowFrom → denied even if chat is listed.
|
|
47
|
+
expect(isAllowedFor(access({ allowFrom: ["1"], allowInChats: ["42"] }), 42, 42)).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("allowInChats empty array fails closed for DMs", () => {
|
|
51
|
+
const a = access({ allowFrom: ["42"], allowInChats: [] });
|
|
52
|
+
expect(isAllowedFor(a, 42, 42)).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("allowInChats absent → user-allowlist only (back-compat, no per-chat gating)", () => {
|
|
56
|
+
const a = access({ allowFrom: ["42"] });
|
|
57
|
+
expect(isAllowedFor(a, 42, 12345)).toBe(true);
|
|
58
|
+
expect(isAllowedFor(a, 42, undefined)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("allowFrom empty + allowlist policy → fail-closed (denies everyone)", () => {
|
|
62
|
+
const a = access({ dmPolicy: "allowlist", allowFrom: [] });
|
|
63
|
+
expect(isAllowedFor(a, 1, 1)).toBe(false);
|
|
64
|
+
expect(isAllowedFor(a, 42, -100)).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Chunking
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
describe("chunkText", () => {
|
|
73
|
+
test("short text → single chunk", () => {
|
|
74
|
+
expect(chunkText("hello", 4096)).toEqual(["hello"]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("long text splits into <=maxLen chunks", () => {
|
|
78
|
+
const text = "a".repeat(10000);
|
|
79
|
+
const chunks = chunkText(text, 4096);
|
|
80
|
+
expect(chunks.length).toBe(3);
|
|
81
|
+
for (const c of chunks) expect(c.length).toBeLessThanOrEqual(4096);
|
|
82
|
+
expect(chunks.join("")).toBe(text);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("prefers newline breaks when one is available in the back half", () => {
|
|
86
|
+
const head = "x".repeat(3000);
|
|
87
|
+
const tail = "y".repeat(3000);
|
|
88
|
+
const chunks = chunkText(`${head}\n${tail}`, 4096);
|
|
89
|
+
expect(chunks[0]).toBe(head); // broke at the newline, which it stripped
|
|
90
|
+
expect(chunks[1]).toBe(tail);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Transport shape
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
describe("TelegramTransport", () => {
|
|
99
|
+
test("throws ChannelConfigError when no config.token (no env fallback)", () => {
|
|
100
|
+
// The daemon-global TELEGRAM_BOT_TOKEN fallback is gone — a telegram channel
|
|
101
|
+
// MUST carry its own per-channel token. Even with the env var set, a config
|
|
102
|
+
// without a token throws: the env is never read as a token source.
|
|
103
|
+
const prev = process.env.TELEGRAM_BOT_TOKEN;
|
|
104
|
+
try {
|
|
105
|
+
// (1) no config token, env UNSET → throws.
|
|
106
|
+
delete process.env.TELEGRAM_BOT_TOKEN;
|
|
107
|
+
expect(() => new TelegramTransport({ name: "tele-x" })).toThrow(ChannelConfigError);
|
|
108
|
+
expect(() => new TelegramTransport({ name: "tele-x" })).toThrow(
|
|
109
|
+
/telegram channel tele-x requires a per-channel bot token/,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// (2) no config token, env SET → STILL throws (env is not a token source).
|
|
113
|
+
process.env.TELEGRAM_BOT_TOKEN = "env-tok";
|
|
114
|
+
expect(() => new TelegramTransport({ name: "tele-x" })).toThrow(ChannelConfigError);
|
|
115
|
+
} finally {
|
|
116
|
+
if (prev === undefined) delete process.env.TELEGRAM_BOT_TOKEN;
|
|
117
|
+
else process.env.TELEGRAM_BOT_TOKEN = prev;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("a per-channel config.token constructs, regardless of the env", () => {
|
|
122
|
+
const prev = process.env.TELEGRAM_BOT_TOKEN;
|
|
123
|
+
try {
|
|
124
|
+
// env UNSET → constructs off the per-channel token.
|
|
125
|
+
delete process.env.TELEGRAM_BOT_TOKEN;
|
|
126
|
+
const perChannel = new TelegramTransport({
|
|
127
|
+
token: "per-channel-tok",
|
|
128
|
+
stateDir: "/tmp/parachute-agent-test-precedence",
|
|
129
|
+
});
|
|
130
|
+
expect(perChannel.kind).toBe("telegram");
|
|
131
|
+
|
|
132
|
+
// env SET to a DIFFERENT value → the per-channel token is what's used; the
|
|
133
|
+
// env is irrelevant, construction still succeeds with the config token.
|
|
134
|
+
process.env.TELEGRAM_BOT_TOKEN = "env-tok";
|
|
135
|
+
const withEnvNoise = new TelegramTransport({
|
|
136
|
+
token: "per-channel-tok",
|
|
137
|
+
stateDir: "/tmp/parachute-agent-test-precedence",
|
|
138
|
+
});
|
|
139
|
+
expect(withEnvNoise.kind).toBe("telegram");
|
|
140
|
+
} finally {
|
|
141
|
+
if (prev === undefined) delete process.env.TELEGRAM_BOT_TOKEN;
|
|
142
|
+
else process.env.TELEGRAM_BOT_TOKEN = prev;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("kind is 'telegram' and outbound methods exist", () => {
|
|
147
|
+
const t = new TelegramTransport({ token: "tok", stateDir: "/tmp/parachute-agent-test-telegram" });
|
|
148
|
+
expect(t.kind).toBe("telegram");
|
|
149
|
+
expect(typeof t.reply).toBe("function");
|
|
150
|
+
expect(typeof t.react).toBe("function");
|
|
151
|
+
expect(typeof t.edit).toBe("function");
|
|
152
|
+
expect(typeof t.sendPermission).toBe("function");
|
|
153
|
+
expect(typeof t.download).toBe("function");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("reply without a chat_id in meta errors clearly", async () => {
|
|
157
|
+
const t = new TelegramTransport({ token: "tok", stateDir: "/tmp/parachute-agent-test-telegram" });
|
|
158
|
+
await expect(t.reply({ channel: "telegram", text: "hi" })).rejects.toThrow(/chat_id is required/);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("sendPermission with no allowlisted users throws ChannelConfigError (→ 400, not 500)", async () => {
|
|
162
|
+
// Fresh state dir → no access.json → default access has empty allowFrom.
|
|
163
|
+
const t = new TelegramTransport({ token: "tok", stateDir: "/tmp/parachute-agent-test-noperm" });
|
|
164
|
+
await expect(
|
|
165
|
+
t.sendPermission({
|
|
166
|
+
channel: "telegram",
|
|
167
|
+
request_id: "abcde",
|
|
168
|
+
tool_name: "Bash",
|
|
169
|
+
description: "run a command",
|
|
170
|
+
input_preview: "ls",
|
|
171
|
+
}),
|
|
172
|
+
).rejects.toBeInstanceOf(ChannelConfigError);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram transport for parachute-agent.
|
|
3
|
+
*
|
|
4
|
+
* Owns everything Telegram: the getUpdates long-poll, message + callback-query
|
|
5
|
+
* handling, attachment download, access control (access.json), the permission
|
|
6
|
+
* inline-keyboard + y/n verdict intercept, and 4096-char chunking.
|
|
7
|
+
*
|
|
8
|
+
* This is the reference Transport implementation. Behavior is preserved
|
|
9
|
+
* verbatim from the pre-refactor daemon — the only change is that inbound
|
|
10
|
+
* messages and permission verdicts now go through `ctx.emit` /
|
|
11
|
+
* `ctx.emitPermissionVerdict` instead of a global broadcast, so the daemon can
|
|
12
|
+
* route them to the right channel's subscribers.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { TelegramApi, type TelegramMessage, type TelegramCallbackQuery } from "../telegram/api.ts";
|
|
16
|
+
import type {
|
|
17
|
+
Transport,
|
|
18
|
+
TransportContext,
|
|
19
|
+
ReplyArgs,
|
|
20
|
+
ReactArgs,
|
|
21
|
+
EditArgs,
|
|
22
|
+
PermissionArgs,
|
|
23
|
+
DownloadArgs,
|
|
24
|
+
} from "../transport.ts";
|
|
25
|
+
import { ChannelConfigError } from "../transport.ts";
|
|
26
|
+
import { defaultStateDir } from "../registry.ts";
|
|
27
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
28
|
+
import { join } from "path";
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Access control — reuse the official plugin's access.json format, plus one
|
|
32
|
+
// parachute-agent extension: `allowInChats`.
|
|
33
|
+
//
|
|
34
|
+
// Fields (all inherited from the official plugin except `allowInChats`):
|
|
35
|
+
// dmPolicy — "open" short-circuits all gating; anything else requires
|
|
36
|
+
// the user to pass `allowFrom`.
|
|
37
|
+
// allowFrom — user-ID allowlist. Checked against `msg.from.id` /
|
|
38
|
+
// `cq.from.id`.
|
|
39
|
+
// allowInChats — OPTIONAL chat-ID allowlist. Two roles:
|
|
40
|
+
// (1) For DMs (positive chat_id === user_id), the chat must
|
|
41
|
+
// be listed AND the user must pass `allowFrom`.
|
|
42
|
+
// (2) For groups (negative chat_id), inclusion grants entry
|
|
43
|
+
// to ANY member of that group — `allowFrom` is bypassed.
|
|
44
|
+
// This lets the agent participate in shared spaces
|
|
45
|
+
// without enumerating every member.
|
|
46
|
+
// Absent → user-allowlist only (backwards-compatible, no
|
|
47
|
+
// per-chat gating, no group bypass). Empty array → FAIL-
|
|
48
|
+
// CLOSED: no chats allowed. To permit a DM while gating
|
|
49
|
+
// groups, list the user's own ID in `allowInChats`.
|
|
50
|
+
// groups, pending — used by the official plugin's pairing flow; read but
|
|
51
|
+
// not otherwise acted on here.
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
export interface AccessConfig {
|
|
55
|
+
dmPolicy: "open" | "pairing" | "allowlist";
|
|
56
|
+
allowFrom: string[];
|
|
57
|
+
allowInChats?: string[];
|
|
58
|
+
groups: Record<string, unknown>;
|
|
59
|
+
pending: Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function loadAccess(accessFile: string): AccessConfig {
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(readFileSync(accessFile, "utf8"));
|
|
65
|
+
} catch {
|
|
66
|
+
return { dmPolicy: "open", allowFrom: [], groups: {}, pending: {} };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Pure access decision, factored out so the policy is unit-testable without a
|
|
72
|
+
* live Telegram connection. See telegram.test.ts.
|
|
73
|
+
*/
|
|
74
|
+
export function isAllowedFor(
|
|
75
|
+
access: AccessConfig,
|
|
76
|
+
userId: number,
|
|
77
|
+
chatId: number | string | undefined,
|
|
78
|
+
): boolean {
|
|
79
|
+
if (access.dmPolicy === "open") return true;
|
|
80
|
+
|
|
81
|
+
// Group-chat bypass: if a group chat (negative chat_id, Telegram convention)
|
|
82
|
+
// is explicitly allowlisted via `allowInChats`, any user who can post in
|
|
83
|
+
// that group may reach the bot. This lets the agent participate in shared
|
|
84
|
+
// spaces like Regen Hub working groups without having to enumerate every
|
|
85
|
+
// member in `allowFrom`. DMs (chat_id === user_id, positive) are NOT
|
|
86
|
+
// covered by this bypass — they still require `allowFrom`.
|
|
87
|
+
const chatIdStr = chatId === undefined ? undefined : String(chatId);
|
|
88
|
+
const isGroup = chatIdStr !== undefined && chatIdStr.startsWith("-");
|
|
89
|
+
if (isGroup && access.allowInChats?.includes(chatIdStr!)) return true;
|
|
90
|
+
|
|
91
|
+
if (!access.allowFrom.includes(String(userId))) return false;
|
|
92
|
+
if (access.allowInChats !== undefined) {
|
|
93
|
+
if (chatIdStr === undefined) return false;
|
|
94
|
+
if (!access.allowInChats.includes(chatIdStr)) return false;
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i;
|
|
100
|
+
const CALLBACK_DATA_RE = /^perm_(allow|deny)_([a-km-z]{5})$/;
|
|
101
|
+
|
|
102
|
+
/** Config for a Telegram transport instance. */
|
|
103
|
+
export interface TelegramTransportConfig {
|
|
104
|
+
/** Per-channel bot token from BotFather. REQUIRED — there is no env fallback. */
|
|
105
|
+
token?: string;
|
|
106
|
+
/** Directory holding access.json + inbox/. Defaults to the channel state dir. */
|
|
107
|
+
stateDir?: string;
|
|
108
|
+
/** Channel name, used only to make the missing-token error message clear. */
|
|
109
|
+
name?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export class TelegramTransport implements Transport {
|
|
113
|
+
readonly kind = "telegram";
|
|
114
|
+
|
|
115
|
+
private api: TelegramApi;
|
|
116
|
+
private accessFile: string;
|
|
117
|
+
private inboxDir: string;
|
|
118
|
+
private offset: number | undefined;
|
|
119
|
+
private pollActive = false;
|
|
120
|
+
private pollTask: Promise<void> | undefined;
|
|
121
|
+
|
|
122
|
+
constructor(config: TelegramTransportConfig = {}) {
|
|
123
|
+
// Per-channel token is REQUIRED — no daemon-global env fallback. Every
|
|
124
|
+
// telegram channel carries its own bot token in its channels.json config.
|
|
125
|
+
const token = config.token;
|
|
126
|
+
if (!token) {
|
|
127
|
+
throw new ChannelConfigError(
|
|
128
|
+
`telegram channel ${config.name ?? "(unnamed)"} requires a per-channel bot token in its config`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const stateDir = config.stateDir ?? defaultStateDir();
|
|
132
|
+
this.api = new TelegramApi(token);
|
|
133
|
+
this.accessFile = join(stateDir, "access.json");
|
|
134
|
+
this.inboxDir = join(stateDir, "inbox");
|
|
135
|
+
mkdirSync(this.inboxDir, { recursive: true });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private isAllowed(userId: number, chatId: number | string | undefined): boolean {
|
|
139
|
+
return isAllowedFor(loadAccess(this.accessFile), userId, chatId);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async start(ctx: TransportContext): Promise<void> {
|
|
143
|
+
this.pollActive = true;
|
|
144
|
+
this.pollTask = this.pollLoop(ctx);
|
|
145
|
+
// Don't await the loop — it runs forever.
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async stop(): Promise<void> {
|
|
149
|
+
this.pollActive = false;
|
|
150
|
+
// The current getUpdates long-poll may still be in flight; we don't await
|
|
151
|
+
// it (it returns within the 30s timeout and then sees pollActive=false).
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private async pollLoop(ctx: TransportContext): Promise<void> {
|
|
155
|
+
try {
|
|
156
|
+
const me = await this.api.getMe();
|
|
157
|
+
console.log(`parachute-agent[${ctx.channel}]: polling as @${me.username}`);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.error(`parachute-agent[${ctx.channel}]: getMe failed:`, err);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
while (this.pollActive) {
|
|
163
|
+
try {
|
|
164
|
+
const updates = await this.api.getUpdates(this.offset, 30);
|
|
165
|
+
for (const update of updates) {
|
|
166
|
+
this.offset = update.update_id + 1;
|
|
167
|
+
if (update.callback_query) {
|
|
168
|
+
await this.handleCallbackQuery(ctx, update.callback_query);
|
|
169
|
+
} else if (update.message) {
|
|
170
|
+
await this.handleMessage(ctx, update.message);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
console.error(`parachute-agent[${ctx.channel}]: poll error:`, err);
|
|
175
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private async handleCallbackQuery(
|
|
181
|
+
ctx: TransportContext,
|
|
182
|
+
cq: TelegramCallbackQuery,
|
|
183
|
+
): Promise<void> {
|
|
184
|
+
const userId = cq.from.id;
|
|
185
|
+
if (!this.isAllowed(userId, cq.message?.chat.id)) {
|
|
186
|
+
await this.api.answerCallbackQuery(cq.id).catch(() => {});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const data = cq.data ?? "";
|
|
191
|
+
const match = CALLBACK_DATA_RE.exec(data);
|
|
192
|
+
if (!match) {
|
|
193
|
+
await this.api.answerCallbackQuery(cq.id).catch(() => {});
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const behavior = match[1] as "allow" | "deny";
|
|
198
|
+
const requestId = match[2]!;
|
|
199
|
+
ctx.emitPermissionVerdict({ request_id: requestId, behavior });
|
|
200
|
+
|
|
201
|
+
// Answer the callback query (stops button loading spinner)
|
|
202
|
+
const label = behavior === "allow" ? "✅ Allowed" : "❌ Denied";
|
|
203
|
+
await this.api.answerCallbackQuery(cq.id, { text: label }).catch(() => {});
|
|
204
|
+
|
|
205
|
+
// Edit the original message to show the outcome and remove buttons
|
|
206
|
+
if (cq.message) {
|
|
207
|
+
const chatId = String(cq.message.chat.id);
|
|
208
|
+
const originalText = cq.message.text ?? "";
|
|
209
|
+
await this.api
|
|
210
|
+
.editMessageText(chatId, cq.message.message_id, `${label}\n\n${originalText}`)
|
|
211
|
+
.catch(() => {});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private async handleMessage(ctx: TransportContext, msg: TelegramMessage): Promise<void> {
|
|
216
|
+
const userId = msg.from?.id;
|
|
217
|
+
if (!userId || !this.isAllowed(userId, msg.chat.id)) return;
|
|
218
|
+
|
|
219
|
+
const userTag = msg.from?.username
|
|
220
|
+
? `@${msg.from.username}`
|
|
221
|
+
: msg.from?.first_name ?? `user ${userId}`;
|
|
222
|
+
console.log(
|
|
223
|
+
`parachute-agent[${ctx.channel}]: rx from ${userTag} in chat ${msg.chat.id}`,
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// Permission-reply intercept: if this looks like "yes xxxxx" or "no xxxxx"
|
|
227
|
+
// for a pending permission request, emit a permission_verdict instead of
|
|
228
|
+
// forwarding as a chat message.
|
|
229
|
+
const text = msg.text ?? "";
|
|
230
|
+
const permMatch = PERMISSION_REPLY_RE.exec(text);
|
|
231
|
+
if (permMatch) {
|
|
232
|
+
const requestId = permMatch[2]!.toLowerCase();
|
|
233
|
+
const behavior = permMatch[1]!.toLowerCase().startsWith("y") ? "allow" : "deny";
|
|
234
|
+
ctx.emitPermissionVerdict({ request_id: requestId, behavior });
|
|
235
|
+
// React to confirm receipt
|
|
236
|
+
const emoji = behavior === "allow" ? "✅" : "❌";
|
|
237
|
+
try {
|
|
238
|
+
await this.api.setMessageReaction(String(msg.chat.id), msg.message_id, emoji);
|
|
239
|
+
} catch {}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Determine attachment info
|
|
244
|
+
let attachmentKind: string | undefined;
|
|
245
|
+
let attachmentFileId: string | undefined;
|
|
246
|
+
let attachmentSize: number | undefined;
|
|
247
|
+
let attachmentMime: string | undefined;
|
|
248
|
+
let imagePath: string | undefined;
|
|
249
|
+
|
|
250
|
+
if (msg.voice) {
|
|
251
|
+
attachmentKind = "voice";
|
|
252
|
+
attachmentFileId = msg.voice.file_id;
|
|
253
|
+
attachmentSize = msg.voice.file_size;
|
|
254
|
+
attachmentMime = msg.voice.mime_type ?? "audio/ogg";
|
|
255
|
+
} else if (msg.audio) {
|
|
256
|
+
attachmentKind = "audio";
|
|
257
|
+
attachmentFileId = msg.audio.file_id;
|
|
258
|
+
attachmentSize = msg.audio.file_size;
|
|
259
|
+
attachmentMime = msg.audio.mime_type;
|
|
260
|
+
} else if (msg.document) {
|
|
261
|
+
attachmentKind = "document";
|
|
262
|
+
attachmentFileId = msg.document.file_id;
|
|
263
|
+
attachmentSize = msg.document.file_size;
|
|
264
|
+
attachmentMime = msg.document.mime_type;
|
|
265
|
+
} else if (msg.photo && msg.photo.length > 0) {
|
|
266
|
+
// Download the largest photo variant and save to inbox
|
|
267
|
+
const largest = msg.photo[msg.photo.length - 1]!;
|
|
268
|
+
attachmentKind = "photo";
|
|
269
|
+
attachmentFileId = largest.file_id;
|
|
270
|
+
attachmentSize = largest.file_size;
|
|
271
|
+
attachmentMime = "image/jpeg";
|
|
272
|
+
try {
|
|
273
|
+
const fileInfo = await this.api.getFile(largest.file_id);
|
|
274
|
+
const data = await this.api.downloadFile(fileInfo.file_path);
|
|
275
|
+
const ext = fileInfo.file_path.split(".").pop() ?? "jpg";
|
|
276
|
+
const localPath = join(this.inboxDir, `${Date.now()}-${largest.file_unique_id}.${ext}`);
|
|
277
|
+
writeFileSync(localPath, data);
|
|
278
|
+
imagePath = localPath;
|
|
279
|
+
} catch (err) {
|
|
280
|
+
console.error(`parachute-agent[${ctx.channel}]: failed to download photo:`, err);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Service events (new_chat_members, left_chat_member, pinned_message,
|
|
285
|
+
// migrate_*, chat-title changes, etc.) arrive as messages with no text,
|
|
286
|
+
// no caption, and no media. We have no way for the agent to act on them,
|
|
287
|
+
// and the previous "(voice message)" placeholder caused the agent to
|
|
288
|
+
// report a nonexistent voice memo on every bot-add. Drop silently.
|
|
289
|
+
if (!msg.text && !msg.caption && !attachmentKind) return;
|
|
290
|
+
const content = msg.text ?? msg.caption ?? `(${attachmentKind})`;
|
|
291
|
+
const ts = new Date(msg.date * 1000).toISOString();
|
|
292
|
+
|
|
293
|
+
// Build the channel notification payload — matches the official plugin's shape
|
|
294
|
+
const meta: Record<string, string> = {
|
|
295
|
+
chat_id: String(msg.chat.id),
|
|
296
|
+
message_id: String(msg.message_id),
|
|
297
|
+
user: msg.from?.username ?? msg.from?.first_name ?? "unknown",
|
|
298
|
+
user_id: String(userId),
|
|
299
|
+
ts,
|
|
300
|
+
};
|
|
301
|
+
if (attachmentKind) meta.attachment_kind = attachmentKind;
|
|
302
|
+
if (attachmentFileId) meta.attachment_file_id = attachmentFileId;
|
|
303
|
+
if (attachmentSize) meta.attachment_size = String(attachmentSize);
|
|
304
|
+
if (attachmentMime) meta.attachment_mime = attachmentMime;
|
|
305
|
+
if (imagePath) meta.image_path = imagePath;
|
|
306
|
+
|
|
307
|
+
ctx.emit({ channel: ctx.channel, content, meta, source: "telegram" });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// -------------------------------------------------------------------------
|
|
311
|
+
// Outbound — Telegram chat_id travels in args.meta.chat_id
|
|
312
|
+
// -------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
private chatIdFrom(meta: Record<string, string> | undefined, field = "chat_id"): string {
|
|
315
|
+
const chatId = meta?.[field];
|
|
316
|
+
if (!chatId) {
|
|
317
|
+
throw new Error(`telegram transport: meta.${field} is required`);
|
|
318
|
+
}
|
|
319
|
+
return chatId;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async reply(args: ReplyArgs): Promise<{ sent: string[] }> {
|
|
323
|
+
const chatId = this.chatIdFrom(args.meta);
|
|
324
|
+
const sentIds: string[] = [];
|
|
325
|
+
|
|
326
|
+
if (args.text) {
|
|
327
|
+
// Chunk long messages (Telegram 4096 char limit)
|
|
328
|
+
const chunks = chunkText(args.text, 4096);
|
|
329
|
+
for (const chunk of chunks) {
|
|
330
|
+
const id = await this.api.sendMessage(
|
|
331
|
+
chatId,
|
|
332
|
+
chunk,
|
|
333
|
+
args.reply_to ? { reply_to_message_id: parseInt(args.reply_to) } : undefined,
|
|
334
|
+
);
|
|
335
|
+
sentIds.push(String(id));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (args.files) {
|
|
340
|
+
for (const filePath of args.files) {
|
|
341
|
+
const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
|
|
342
|
+
const imageExts = ["jpg", "jpeg", "png", "gif", "webp"];
|
|
343
|
+
const voiceExts = ["ogg", "oga", "opus"];
|
|
344
|
+
let id: number;
|
|
345
|
+
if (imageExts.includes(ext)) {
|
|
346
|
+
id = await this.api.sendPhoto(chatId, filePath);
|
|
347
|
+
} else if (voiceExts.includes(ext)) {
|
|
348
|
+
id = await this.api.sendVoice(chatId, filePath);
|
|
349
|
+
} else {
|
|
350
|
+
id = await this.api.sendDocument(chatId, filePath);
|
|
351
|
+
}
|
|
352
|
+
sentIds.push(String(id));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return { sent: sentIds };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async react(args: ReactArgs): Promise<void> {
|
|
360
|
+
const chatId = this.chatIdFrom(args.meta);
|
|
361
|
+
await this.api.setMessageReaction(chatId, parseInt(args.message_id), args.emoji);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async edit(args: EditArgs): Promise<void> {
|
|
365
|
+
const chatId = this.chatIdFrom(args.meta);
|
|
366
|
+
await this.api.editMessageText(chatId, parseInt(args.message_id), args.text);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async sendPermission(args: PermissionArgs): Promise<{ sent: string[] }> {
|
|
370
|
+
const access = loadAccess(this.accessFile);
|
|
371
|
+
const targets = access.allowFrom;
|
|
372
|
+
if (targets.length === 0) {
|
|
373
|
+
throw new ChannelConfigError("no allowlisted users to send permission prompt to");
|
|
374
|
+
}
|
|
375
|
+
const text =
|
|
376
|
+
`🔐 Permission: ${args.tool_name}\n\n` +
|
|
377
|
+
`${args.description}\n\n` +
|
|
378
|
+
`${args.input_preview}`;
|
|
379
|
+
const replyMarkup = {
|
|
380
|
+
inline_keyboard: [
|
|
381
|
+
[
|
|
382
|
+
{ text: "✅ Allow", callback_data: `perm_allow_${args.request_id}` },
|
|
383
|
+
{ text: "❌ Deny", callback_data: `perm_deny_${args.request_id}` },
|
|
384
|
+
],
|
|
385
|
+
],
|
|
386
|
+
};
|
|
387
|
+
const sent: string[] = [];
|
|
388
|
+
for (const chatId of targets) {
|
|
389
|
+
try {
|
|
390
|
+
const id = await this.api.sendMessage(chatId, text, { reply_markup: replyMarkup });
|
|
391
|
+
sent.push(String(id));
|
|
392
|
+
} catch (err) {
|
|
393
|
+
console.error(`parachute-agent: permission prompt to ${chatId} failed:`, err);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return { sent };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async download(args: DownloadArgs): Promise<{ path: string }> {
|
|
400
|
+
const fileInfo = await this.api.getFile(args.file_id);
|
|
401
|
+
const data = await this.api.downloadFile(fileInfo.file_path);
|
|
402
|
+
const ext = fileInfo.file_path.split(".").pop() ?? "bin";
|
|
403
|
+
const localPath = join(this.inboxDir, `${Date.now()}-${args.file_id.slice(-10)}.${ext}`);
|
|
404
|
+
writeFileSync(localPath, data);
|
|
405
|
+
return { path: localPath };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Split text into <=maxLen chunks, preferring newline breaks. Exported for tests. */
|
|
410
|
+
export function chunkText(text: string, maxLen: number): string[] {
|
|
411
|
+
if (text.length <= maxLen) return [text];
|
|
412
|
+
const chunks: string[] = [];
|
|
413
|
+
let remaining = text;
|
|
414
|
+
while (remaining.length > 0) {
|
|
415
|
+
if (remaining.length <= maxLen) {
|
|
416
|
+
chunks.push(remaining);
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
// Try to break at a newline
|
|
420
|
+
let breakAt = remaining.lastIndexOf("\n", maxLen);
|
|
421
|
+
if (breakAt < maxLen * 0.5) breakAt = maxLen; // no good newline, hard break
|
|
422
|
+
chunks.push(remaining.slice(0, breakAt));
|
|
423
|
+
remaining = remaining.slice(breakAt).replace(/^\n/, "");
|
|
424
|
+
}
|
|
425
|
+
return chunks;
|
|
426
|
+
}
|