@openparachute/agent 0.1.2 → 0.2.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/.parachute/module.json +124 -8
- package/LICENSE +2 -16
- package/README.md +118 -166
- package/package.json +32 -43
- 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/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
package/src/jobs.ts
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduled-job model + VAULT-NATIVE store for the runner (design
|
|
3
|
+
* `2026-06-17-runner-scheduled-agent-turns.md`).
|
|
4
|
+
*
|
|
5
|
+
* A scheduled job is "an automated human": send message M to agent (channel) A on
|
|
6
|
+
* schedule S. The runner does NOT execute anything — it authors an inbound
|
|
7
|
+
* `#agent/message/inbound` note on a schedule, and the existing vault trigger →
|
|
8
|
+
* agent-turn → outbound flow does the rest.
|
|
9
|
+
*
|
|
10
|
+
* STORAGE IS VAULT-NATIVE (Aaron's call, 2026-06-17): a job IS a `#agent/job`
|
|
11
|
+
* note in the TARGET channel's vault — durable, queryable, and renderable by any
|
|
12
|
+
* surface, converging with the blueprint's "vault as the spine" and the future
|
|
13
|
+
* `tag:job` idea. There is NO jobs.json. The vault note I/O lives on
|
|
14
|
+
* `VaultTransport` (it owns the vault URL + token + encoding); this module is the
|
|
15
|
+
* thin, storage-agnostic FACADE that keeps the same read-all / upsert / remove
|
|
16
|
+
* interface the runner + API call. Token handling is never duplicated here.
|
|
17
|
+
*
|
|
18
|
+
* `metadata` on the note is all string-typed (the vault stores metadata as
|
|
19
|
+
* strings): `{ channel, cron, tz?, enabled, createdAt, lastRunAt?, lastStatus? }`.
|
|
20
|
+
* `nextRunAt` is NOT persisted — the runner computes it in memory each tick.
|
|
21
|
+
*
|
|
22
|
+
* `validateJob` is the pure gate the API runs before writing: slug-shaped id, a
|
|
23
|
+
* known channel that's a VAULT transport, and a parseable cron.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { parseCron, CronParseError } from "./cron.ts";
|
|
27
|
+
import { VaultTransport } from "./transports/vault.ts";
|
|
28
|
+
import type { Channel } from "./registry.ts";
|
|
29
|
+
|
|
30
|
+
/** A job's schedule: a 5-field cron expr + optional IANA tz (default daemon-local). */
|
|
31
|
+
export interface JobSchedule {
|
|
32
|
+
/** 5-field cron: `min hour dom mon dow`. */
|
|
33
|
+
cron: string;
|
|
34
|
+
/** IANA timezone (e.g. "America/Los_Angeles"). Optional — default daemon-local. */
|
|
35
|
+
tz?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* One scheduled job. The in-memory shape the runner + API operate on; persisted as
|
|
40
|
+
* a `#agent/job` vault note (see the store below). `id` is the slug (also the
|
|
41
|
+
* `runner:<id>` sender provenance + the note path segment).
|
|
42
|
+
*/
|
|
43
|
+
export interface Job {
|
|
44
|
+
/** The operator-facing slug (typed on create; addresses the job in `/api/jobs/:id`). */
|
|
45
|
+
id: string;
|
|
46
|
+
/**
|
|
47
|
+
* The vault note id/path that addresses the persisted note for PATCH/DELETE.
|
|
48
|
+
* Absent on a freshly-created in-memory `Job` (set after `upsert` / on `listAll`).
|
|
49
|
+
* The runner + UI key off `id` (the slug); the store uses `noteId` for I/O.
|
|
50
|
+
*/
|
|
51
|
+
noteId?: string;
|
|
52
|
+
/** The channel to inject into — MUST be a vault channel. */
|
|
53
|
+
channel: string;
|
|
54
|
+
/** The message text written as the inbound note content (= the job note's content). */
|
|
55
|
+
message: string;
|
|
56
|
+
/** When to fire. */
|
|
57
|
+
schedule: JobSchedule;
|
|
58
|
+
/** Whether the runner considers this job (default true on create). */
|
|
59
|
+
enabled: boolean;
|
|
60
|
+
/** ISO timestamp the job was created. */
|
|
61
|
+
createdAt: string;
|
|
62
|
+
/** ISO timestamp of the most recent fire (set by the runner; persisted via PATCH). */
|
|
63
|
+
lastRunAt?: string;
|
|
64
|
+
/** "ok" or "error: <detail>" from the most recent fire (persisted via PATCH). */
|
|
65
|
+
lastStatus?: string;
|
|
66
|
+
/** ISO timestamp of the next scheduled fire — COMPUTED IN MEMORY, never persisted. */
|
|
67
|
+
nextRunAt?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A slug: alphanumeric, dash, underscore (same shape as channel names). */
|
|
71
|
+
const SLUG_RE = /^[a-zA-Z0-9_-]+$/;
|
|
72
|
+
|
|
73
|
+
/** The result of validating a candidate job. `ok:false` carries an operator-facing reason. */
|
|
74
|
+
export type JobValidation = { ok: true } | { ok: false; error: string };
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validate a candidate job before it's persisted. The API runs this and maps an
|
|
78
|
+
* `ok:false` to a 400. Pure (no vault I/O) — `isVaultChannel(name)` is injected:
|
|
79
|
+
* - returns `true` → known + vault,
|
|
80
|
+
* - returns `false` → known but NOT vault,
|
|
81
|
+
* - returns `null` → unknown channel.
|
|
82
|
+
*
|
|
83
|
+
* Checks, in order: id slug → non-empty message → parseable cron → valid tz (if
|
|
84
|
+
* present) → channel known AND vault (the inject path is "write an inbound note,"
|
|
85
|
+
* which only a vault transport supports).
|
|
86
|
+
*/
|
|
87
|
+
export function validateJob(
|
|
88
|
+
candidate: {
|
|
89
|
+
id?: unknown;
|
|
90
|
+
channel?: unknown;
|
|
91
|
+
message?: unknown;
|
|
92
|
+
schedule?: unknown;
|
|
93
|
+
enabled?: unknown;
|
|
94
|
+
},
|
|
95
|
+
isVaultChannel: (name: string) => boolean | null,
|
|
96
|
+
): JobValidation {
|
|
97
|
+
if (typeof candidate.id !== "string" || !SLUG_RE.test(candidate.id)) {
|
|
98
|
+
return { ok: false, error: "id must be a slug (alphanumeric, dash, underscore)" };
|
|
99
|
+
}
|
|
100
|
+
if (typeof candidate.message !== "string" || candidate.message.trim().length === 0) {
|
|
101
|
+
return { ok: false, error: "message must be a non-empty string" };
|
|
102
|
+
}
|
|
103
|
+
const sched = candidate.schedule as { cron?: unknown; tz?: unknown } | undefined;
|
|
104
|
+
if (!sched || typeof sched.cron !== "string" || sched.cron.trim().length === 0) {
|
|
105
|
+
return { ok: false, error: "schedule.cron must be a non-empty cron expression" };
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
parseCron(sched.cron);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
const msg = err instanceof CronParseError ? err.message : String(err);
|
|
111
|
+
return { ok: false, error: `invalid schedule.cron: ${msg}` };
|
|
112
|
+
}
|
|
113
|
+
if (sched.tz !== undefined) {
|
|
114
|
+
if (typeof sched.tz !== "string" || sched.tz.length === 0) {
|
|
115
|
+
return { ok: false, error: "schedule.tz must be a non-empty IANA timezone string" };
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
// Construct a formatter to validate the zone — throws RangeError if invalid.
|
|
119
|
+
new Intl.DateTimeFormat("en-US", { timeZone: sched.tz });
|
|
120
|
+
} catch {
|
|
121
|
+
return { ok: false, error: `invalid schedule.tz: "${sched.tz}" is not a known IANA timezone` };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (typeof candidate.channel !== "string" || candidate.channel.length === 0) {
|
|
125
|
+
return { ok: false, error: "channel must be a non-empty string" };
|
|
126
|
+
}
|
|
127
|
+
const vault = isVaultChannel(candidate.channel);
|
|
128
|
+
if (vault === null) {
|
|
129
|
+
return { ok: false, error: `unknown channel "${candidate.channel}"` };
|
|
130
|
+
}
|
|
131
|
+
if (vault === false) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
error: `channel "${candidate.channel}" is not a vault channel — scheduled jobs require a vault-backed agent (the runner injects an inbound note)`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return { ok: true };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Resolve a channel name to its live `VaultTransport`, or null if the channel is
|
|
142
|
+
* unknown or not vault-backed. Shared by the store + the runner's discovery so the
|
|
143
|
+
* "is this a vault channel?" check is one implementation.
|
|
144
|
+
*/
|
|
145
|
+
export function vaultTransportFor(
|
|
146
|
+
channels: Map<string, Channel>,
|
|
147
|
+
name: string,
|
|
148
|
+
): VaultTransport | null {
|
|
149
|
+
const t = channels.get(name)?.transport;
|
|
150
|
+
return t instanceof VaultTransport ? t : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The vault-native job store. Same read-all / upsert / remove interface the file
|
|
155
|
+
* store had, now backed by `#agent/job` vault notes via the channel's
|
|
156
|
+
* `VaultTransport`. Each method resolves the target channel's vault transport (the
|
|
157
|
+
* channel carries the vault binding + write token) and delegates the I/O to it.
|
|
158
|
+
*
|
|
159
|
+
* `listAll` queries every UNIQUE vault among the live vault-channels once and maps
|
|
160
|
+
* each job note to a `Job`, routing by `metadata.channel`. A job note whose
|
|
161
|
+
* `channel` no longer names a live vault channel is still RETURNED (so the API/UI
|
|
162
|
+
* can show + let the operator delete a stale job), but the runner's discovery
|
|
163
|
+
* (which composes on top) skips firing it.
|
|
164
|
+
*/
|
|
165
|
+
export class VaultJobStore {
|
|
166
|
+
constructor(private readonly channels: Map<string, Channel>) {}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* List all scheduled jobs across every vault the live channels point at. We
|
|
170
|
+
* query each DISTINCT vault transport once (dedup by vault URL + name), then map
|
|
171
|
+
* job notes to `Job`s. A note read from one vault may target ANY channel on that
|
|
172
|
+
* vault (routing is by `metadata.channel`), so we keep every well-formed job
|
|
173
|
+
* note. De-dup by job id across vaults isn't needed — ids are namespaced by the
|
|
174
|
+
* channel's vault in practice — but if two notes share an id we keep both (the
|
|
175
|
+
* runner routes each by its own `channel`).
|
|
176
|
+
*/
|
|
177
|
+
async listAll(): Promise<Job[]> {
|
|
178
|
+
// Dedup the vaults we query by vault IDENTITY (origin + name), NOT transport
|
|
179
|
+
// instance: many channels each construct their OWN VaultTransport pointing at the
|
|
180
|
+
// SAME vault, so instance-identity dedup misses and the same job notes come back
|
|
181
|
+
// once per channel sharing that vault. Key on `vaultKey()`. (Caught live
|
|
182
|
+
// 2026-06-18: one job listed 3x with three channels on the default vault.)
|
|
183
|
+
const seen = new Set<string>();
|
|
184
|
+
const transports: VaultTransport[] = [];
|
|
185
|
+
for (const ch of this.channels.values()) {
|
|
186
|
+
if (ch.transport instanceof VaultTransport && !seen.has(ch.transport.vaultKey())) {
|
|
187
|
+
seen.add(ch.transport.vaultKey());
|
|
188
|
+
transports.push(ch.transport);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const jobs: Job[] = [];
|
|
192
|
+
for (const t of transports) {
|
|
193
|
+
const notes = await t.listJobNotes();
|
|
194
|
+
for (const n of notes) {
|
|
195
|
+
jobs.push({
|
|
196
|
+
id: n.id, // the operator-facing slug (metadata.jobId)
|
|
197
|
+
noteId: n.noteId, // the vault note id/path — for PATCH/DELETE
|
|
198
|
+
channel: n.channel,
|
|
199
|
+
message: n.message,
|
|
200
|
+
schedule: { cron: n.cron, ...(n.tz ? { tz: n.tz } : {}) },
|
|
201
|
+
enabled: n.enabled,
|
|
202
|
+
createdAt: n.createdAt ?? "",
|
|
203
|
+
...(n.lastRunAt ? { lastRunAt: n.lastRunAt } : {}),
|
|
204
|
+
...(n.lastStatus ? { lastStatus: n.lastStatus } : {}),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return jobs;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Create or replace a job (by slug id) as a `#agent/job` note in its target
|
|
213
|
+
* channel's vault. Throws if the target channel isn't a live vault channel
|
|
214
|
+
* (the API validates this first, so it's a guard, not the primary check).
|
|
215
|
+
* Returns the persisted job with its `noteId` filled in (the `id` stays the slug).
|
|
216
|
+
*/
|
|
217
|
+
async upsert(job: Job): Promise<Job> {
|
|
218
|
+
const t = vaultTransportFor(this.channels, job.channel);
|
|
219
|
+
if (!t) {
|
|
220
|
+
throw new Error(`cannot store job "${job.id}": channel "${job.channel}" is not a live vault channel`);
|
|
221
|
+
}
|
|
222
|
+
const { id: noteId } = await t.upsertJobNote({
|
|
223
|
+
id: job.id,
|
|
224
|
+
message: job.message,
|
|
225
|
+
channel: job.channel,
|
|
226
|
+
cron: job.schedule.cron,
|
|
227
|
+
...(job.schedule.tz ? { tz: job.schedule.tz } : {}),
|
|
228
|
+
enabled: job.enabled,
|
|
229
|
+
createdAt: job.createdAt,
|
|
230
|
+
...(job.lastRunAt ? { lastRunAt: job.lastRunAt } : {}),
|
|
231
|
+
...(job.lastStatus ? { lastStatus: job.lastStatus } : {}),
|
|
232
|
+
});
|
|
233
|
+
return { ...job, noteId }; // id stays the slug; noteId addresses the persisted note.
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Delete a job by its vault note id/path. The job lives in ITS channel's vault,
|
|
238
|
+
* so we need that channel to resolve the right transport. The API passes the
|
|
239
|
+
* job's `noteId` + channel (it has the job in hand from a prior list). Throws on
|
|
240
|
+
* a non-ok vault response.
|
|
241
|
+
*/
|
|
242
|
+
async remove(noteId: string, channel: string): Promise<void> {
|
|
243
|
+
const t = vaultTransportFor(this.channels, channel);
|
|
244
|
+
if (!t) {
|
|
245
|
+
throw new Error(`cannot delete job in channel "${channel}": not a live vault channel`);
|
|
246
|
+
}
|
|
247
|
+
await t.deleteJobNote(noteId);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* PATCH a job's bookkeeping (lastRunAt / lastStatus / enabled) onto its vault
|
|
252
|
+
* note. Used by the runner after a fire. Throws on a non-ok vault response (the
|
|
253
|
+
* runner swallows it — status persistence is best-effort).
|
|
254
|
+
*/
|
|
255
|
+
async patch(
|
|
256
|
+
noteId: string,
|
|
257
|
+
channel: string,
|
|
258
|
+
fields: { lastRunAt?: string; lastStatus?: string; enabled?: boolean },
|
|
259
|
+
): Promise<void> {
|
|
260
|
+
const t = vaultTransportFor(this.channels, channel);
|
|
261
|
+
if (!t) {
|
|
262
|
+
throw new Error(`cannot patch job in channel "${channel}": not a live vault channel`);
|
|
263
|
+
}
|
|
264
|
+
await t.patchJobNote(noteId, fields);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP MCP tests — the per-channel session registry, the wake push, write-scope
|
|
3
|
+
* enforcement on tool dispatch, and the daemon's /mcp/<channel> auth gate.
|
|
4
|
+
*
|
|
5
|
+
* Like daemon.test.ts these need NO live hub: the no-token path in requireScope
|
|
6
|
+
* short-circuits before any JWKS fetch, and the registry/push/tool tests drive
|
|
7
|
+
* the in-memory session set directly with fake servers + a fake transport.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, test, expect, afterEach, beforeAll, afterAll } from "bun:test";
|
|
10
|
+
import {
|
|
11
|
+
pushToChannel,
|
|
12
|
+
pushPermissionVerdict,
|
|
13
|
+
mcpSessionCount,
|
|
14
|
+
assertMcpSdkStreamContract,
|
|
15
|
+
_resetSessionsForTest,
|
|
16
|
+
} from "./mcp-http.ts";
|
|
17
|
+
import { createFetchHandler } from "./daemon.ts";
|
|
18
|
+
import { ClientRegistry } from "./routing.ts";
|
|
19
|
+
import { HttpUiTransport } from "./transports/http-ui.ts";
|
|
20
|
+
import type { Channel } from "./registry.ts";
|
|
21
|
+
import type { Transport, ReplyArgs } from "./transport.ts";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Registry + wake — drive the session set directly via a registration shim.
|
|
25
|
+
//
|
|
26
|
+
// pushToChannel iterates the channel's session set and calls
|
|
27
|
+
// server.notification. We register fake sessions by reaching the same internal
|
|
28
|
+
// maps the way handleMcp's onsessioninitialized would. Since those maps are
|
|
29
|
+
// module-private, we exercise them through the public surface: register via a
|
|
30
|
+
// tiny test-only re-entry that mirrors registerSession. To keep this hermetic
|
|
31
|
+
// without exporting internals, we register by spinning handleMcp is overkill;
|
|
32
|
+
// instead we assert push reaches a registered session through a captured
|
|
33
|
+
// server.notification. We use the exported _registerForTest below.
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
import { _registerSessionForTest, _unregisterSessionForTest } from "./mcp-http.ts";
|
|
37
|
+
|
|
38
|
+
interface FakeServer {
|
|
39
|
+
notes: Array<{ method: string; params: unknown }>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fakeSession(): { server: { notification: (n: unknown) => void }; captured: FakeServer } {
|
|
43
|
+
const captured: FakeServer = { notes: [] };
|
|
44
|
+
const server = {
|
|
45
|
+
notification(n: unknown) {
|
|
46
|
+
captured.notes.push(n as { method: string; params: unknown });
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
return { server, captured };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
_resetSessionsForTest();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("per-channel MCP session registry + wake push", () => {
|
|
57
|
+
test("pushToChannel reaches a session on A and NOT one on B", () => {
|
|
58
|
+
const a = fakeSession();
|
|
59
|
+
const b = fakeSession();
|
|
60
|
+
_registerSessionForTest("A", "sid-a", a.server as never, ["agent:read", "agent:write"]);
|
|
61
|
+
_registerSessionForTest("B", "sid-b", b.server as never, ["agent:read"]);
|
|
62
|
+
|
|
63
|
+
const delivered = pushToChannel("A", "hello A", { foo: "bar" });
|
|
64
|
+
expect(delivered).toBe(1);
|
|
65
|
+
expect(a.captured.notes).toHaveLength(1);
|
|
66
|
+
expect(a.captured.notes[0]!.method).toBe("notifications/claude/agent");
|
|
67
|
+
expect((a.captured.notes[0]!.params as { content: string }).content).toBe("hello A");
|
|
68
|
+
// Source is stamped + caller meta merged.
|
|
69
|
+
expect((a.captured.notes[0]!.params as { meta: Record<string, string> }).meta).toMatchObject({
|
|
70
|
+
source: "parachute-agent",
|
|
71
|
+
foo: "bar",
|
|
72
|
+
});
|
|
73
|
+
// B got nothing.
|
|
74
|
+
expect(b.captured.notes).toHaveLength(0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("two sessions on the same channel both get the wake", () => {
|
|
78
|
+
const a1 = fakeSession();
|
|
79
|
+
const a2 = fakeSession();
|
|
80
|
+
_registerSessionForTest("A", "sid-a1", a1.server as never, ["agent:read"]);
|
|
81
|
+
_registerSessionForTest("A", "sid-a2", a2.server as never, ["agent:read"]);
|
|
82
|
+
expect(mcpSessionCount("A")).toBe(2);
|
|
83
|
+
const delivered = pushToChannel("A", "broadcast", {});
|
|
84
|
+
expect(delivered).toBe(2);
|
|
85
|
+
expect(a1.captured.notes).toHaveLength(1);
|
|
86
|
+
expect(a2.captured.notes).toHaveLength(1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("pushPermissionVerdict pushes the permission method", () => {
|
|
90
|
+
const a = fakeSession();
|
|
91
|
+
_registerSessionForTest("A", "sid-a", a.server as never, ["agent:read"]);
|
|
92
|
+
const delivered = pushPermissionVerdict("A", { request_id: "r1", behavior: "allow" });
|
|
93
|
+
expect(delivered).toBe(1);
|
|
94
|
+
expect(a.captured.notes[0]!.method).toBe("notifications/claude/agent/permission");
|
|
95
|
+
expect(a.captured.notes[0]!.params).toMatchObject({ request_id: "r1", behavior: "allow" });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("push to an unknown channel delivers to nobody (0)", () => {
|
|
99
|
+
expect(pushToChannel("nope", "x", {})).toBe(0);
|
|
100
|
+
expect(pushPermissionVerdict("nope", { request_id: "r", behavior: "deny" })).toBe(0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("a streamless session (registered, no live GET stream) is NOT counted as delivered", () => {
|
|
104
|
+
// The bug this guards: a session that POSTed `initialize` but hasn't opened (or
|
|
105
|
+
// has dropped) its standalone GET stream is registered, but the SDK silently
|
|
106
|
+
// drops any notification to it. If pushToChannel counted it, the daemon would
|
|
107
|
+
// advance the channel's delivery mark and the message would be lost.
|
|
108
|
+
const a = fakeSession();
|
|
109
|
+
_registerSessionForTest("A", "sid-streamless", a.server as never, ["agent:read"], {
|
|
110
|
+
streamless: true,
|
|
111
|
+
});
|
|
112
|
+
expect(mcpSessionCount("A")).toBe(1); // it IS registered…
|
|
113
|
+
expect(pushToChannel("A", "into the void", {})).toBe(0); // …but NOT deliverable
|
|
114
|
+
expect(a.captured.notes).toHaveLength(0); // not even attempted
|
|
115
|
+
// Permission verdicts honor the same gate.
|
|
116
|
+
expect(pushPermissionVerdict("A", { request_id: "r", behavior: "allow" })).toBe(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("pushToChannel counts only the live-stream sessions in a mixed set", () => {
|
|
120
|
+
const live = fakeSession();
|
|
121
|
+
const dead = fakeSession();
|
|
122
|
+
_registerSessionForTest("A", "sid-live", live.server as never, ["agent:read"]);
|
|
123
|
+
_registerSessionForTest("A", "sid-streamless", dead.server as never, ["agent:read"], {
|
|
124
|
+
streamless: true,
|
|
125
|
+
});
|
|
126
|
+
expect(mcpSessionCount("A")).toBe(2); // both registered
|
|
127
|
+
expect(pushToChannel("A", "hi", {})).toBe(1); // only the one with a live stream
|
|
128
|
+
expect(live.captured.notes).toHaveLength(1);
|
|
129
|
+
expect(dead.captured.notes).toHaveLength(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("the installed MCP SDK still keys the standalone GET stream as we expect (contract guard)", () => {
|
|
133
|
+
// If this fails, the SDK renamed the internal sessionHasLivePushStream reads —
|
|
134
|
+
// HTTP-MCP delivery would silently break. Catch it here, not in production.
|
|
135
|
+
expect(assertMcpSdkStreamContract()).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("mcpSessionCount tracks registration + reset", () => {
|
|
139
|
+
expect(mcpSessionCount("A")).toBe(0);
|
|
140
|
+
const a = fakeSession();
|
|
141
|
+
_registerSessionForTest("A", "sid-a", a.server as never, ["agent:read"]);
|
|
142
|
+
expect(mcpSessionCount("A")).toBe(1);
|
|
143
|
+
_resetSessionsForTest();
|
|
144
|
+
expect(mcpSessionCount("A")).toBe(0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("unregister cleans up the session and drops the empty channel set (no leak)", () => {
|
|
148
|
+
_registerSessionForTest("A", "sid-a1", fakeSession().server as never, ["agent:read"]);
|
|
149
|
+
_registerSessionForTest("A", "sid-a2", fakeSession().server as never, ["agent:read"]);
|
|
150
|
+
expect(mcpSessionCount("A")).toBe(2);
|
|
151
|
+
_unregisterSessionForTest("A", "sid-a1");
|
|
152
|
+
expect(mcpSessionCount("A")).toBe(1); // the other session survives
|
|
153
|
+
_unregisterSessionForTest("A", "sid-a2");
|
|
154
|
+
expect(mcpSessionCount("A")).toBe(0); // empty set removed — no orphaned channel entry
|
|
155
|
+
// a push to the now-cleaned channel reaches nobody
|
|
156
|
+
expect(pushToChannel("A", "hi", {})).toBe(0);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Tool dispatch — a reply tool call routes to the channel's transport.
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
describe("tool dispatch routes to the channel's transport + enforces write scope", () => {
|
|
165
|
+
function fakeTransport(): { transport: Transport; replies: ReplyArgs[] } {
|
|
166
|
+
const replies: ReplyArgs[] = [];
|
|
167
|
+
const transport: Transport = {
|
|
168
|
+
kind: "fake",
|
|
169
|
+
async start() {},
|
|
170
|
+
async stop() {},
|
|
171
|
+
async reply(args: ReplyArgs) {
|
|
172
|
+
replies.push(args);
|
|
173
|
+
return { sent: ["msg-1"] };
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
return { transport, replies };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
test("a write-scoped reply call reaches transport.reply with the channel + args", async () => {
|
|
180
|
+
const { transport, replies } = fakeTransport();
|
|
181
|
+
const { callReplyTool } = await import("./mcp-http.ts");
|
|
182
|
+
const result = await callReplyTool("A", transport, ["agent:read", "agent:write"], {
|
|
183
|
+
text: "hi there",
|
|
184
|
+
chat_id: "42",
|
|
185
|
+
});
|
|
186
|
+
expect(result.isError).toBeUndefined();
|
|
187
|
+
expect(replies).toHaveLength(1);
|
|
188
|
+
expect(replies[0]).toMatchObject({ channel: "A", text: "hi there", meta: { chat_id: "42" } });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("a read-only token cannot call reply (write scope enforced)", async () => {
|
|
192
|
+
const { transport, replies } = fakeTransport();
|
|
193
|
+
const { callReplyTool } = await import("./mcp-http.ts");
|
|
194
|
+
const result = await callReplyTool("A", transport, ["agent:read"], { text: "blocked" });
|
|
195
|
+
expect(result.isError).toBe(true);
|
|
196
|
+
expect(replies).toHaveLength(0);
|
|
197
|
+
expect((result.content[0] as { text: string }).text).toContain("agent:write");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("DUAL-ACCEPT: a pre-rename token with the LEGACY channel:write scope can still call reply", async () => {
|
|
201
|
+
// The write-tool gate must dual-accept (grantsScope), not raw-includes — else
|
|
202
|
+
// a pre-rename token connects + is woken but silently can't send (channel#…).
|
|
203
|
+
const { transport, replies } = fakeTransport();
|
|
204
|
+
const { callReplyTool } = await import("./mcp-http.ts");
|
|
205
|
+
const result = await callReplyTool("A", transport, ["channel:read", "channel:write"], {
|
|
206
|
+
text: "legacy-send",
|
|
207
|
+
});
|
|
208
|
+
expect(result.isError).toBeUndefined();
|
|
209
|
+
expect(replies).toHaveLength(1);
|
|
210
|
+
expect(replies[0]).toMatchObject({ channel: "A", text: "legacy-send" });
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Daemon auth gate — POST /mcp/<channel> with no bearer → 401 (pre-JWKS).
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
describe("daemon /mcp/<channel> auth gate", () => {
|
|
219
|
+
let server: ReturnType<typeof Bun.serve>;
|
|
220
|
+
let base: string;
|
|
221
|
+
|
|
222
|
+
beforeAll(async () => {
|
|
223
|
+
const registry = new ClientRegistry();
|
|
224
|
+
const transport = new HttpUiTransport({ channel: "ui1" });
|
|
225
|
+
await transport.start({ channel: "ui1", emit: () => {}, emitPermissionVerdict: () => {} });
|
|
226
|
+
const channels = new Map<string, Channel>([
|
|
227
|
+
["ui1", { name: "ui1", transport, entry: { name: "ui1", transport: "http-ui" } }],
|
|
228
|
+
]);
|
|
229
|
+
server = Bun.serve({
|
|
230
|
+
port: 0,
|
|
231
|
+
hostname: "127.0.0.1",
|
|
232
|
+
idleTimeout: 0,
|
|
233
|
+
fetch: createFetchHandler(channels, registry),
|
|
234
|
+
});
|
|
235
|
+
base = `http://127.0.0.1:${server.port}`;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
afterAll(() => {
|
|
239
|
+
server.stop(true);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("POST /mcp/ui1 with an initialize body and no bearer → 401", async () => {
|
|
243
|
+
const res = await fetch(`${base}/mcp/ui1`, {
|
|
244
|
+
method: "POST",
|
|
245
|
+
headers: { "content-type": "application/json", accept: "application/json, text/event-stream" },
|
|
246
|
+
body: JSON.stringify({
|
|
247
|
+
jsonrpc: "2.0",
|
|
248
|
+
id: 1,
|
|
249
|
+
method: "initialize",
|
|
250
|
+
params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "t", version: "0" } },
|
|
251
|
+
}),
|
|
252
|
+
});
|
|
253
|
+
expect(res.status).toBe(401);
|
|
254
|
+
expect(((await res.json()) as { error: string }).error).toBe("unauthorized");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("POST /mcp/unknown-channel → 404 (channel miss, before auth body)", async () => {
|
|
258
|
+
const res = await fetch(`${base}/mcp/does-not-exist`, {
|
|
259
|
+
method: "POST",
|
|
260
|
+
headers: { "content-type": "application/json" },
|
|
261
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} }),
|
|
262
|
+
});
|
|
263
|
+
expect(res.status).toBe(404);
|
|
264
|
+
});
|
|
265
|
+
});
|