@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
package/src/runner.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runner — a scheduler that fires scheduled jobs (design
|
|
3
|
+
* `2026-06-17-runner-scheduled-agent-turns.md`).
|
|
4
|
+
*
|
|
5
|
+
* It does NOT execute anything. On each tick it loads the current jobs (from the
|
|
6
|
+
* VAULT-NATIVE store), asks each enabled job "are you due?" and, if so, FIRES it —
|
|
7
|
+
* where "fire" means "inject an inbound note onto the job's vault channel." The
|
|
8
|
+
* existing vault trigger → agent-turn → outbound flow does all the work. The
|
|
9
|
+
* runner is a clock that authors messages.
|
|
10
|
+
*
|
|
11
|
+
* Determinism is the design's hard requirement: the testable core takes an
|
|
12
|
+
* INJECTABLE clock (`now`), INJECTABLE load + fire + persist fns, and an
|
|
13
|
+
* INJECTABLE tick driver. The daemon's boot supplies the real ones (`Date`, the
|
|
14
|
+
* vault job store, the vault-inject fire, a `setInterval` tick); tests supply
|
|
15
|
+
* fakes and step time by hand. No real `setInterval`/`Date.now()` appears in
|
|
16
|
+
* `tick()` itself.
|
|
17
|
+
*
|
|
18
|
+
* Storage-agnostic: the runner never touches the vault directly. It calls:
|
|
19
|
+
* - `loadJobs()` → the current jobs (the store queries the vault),
|
|
20
|
+
* - `fire(job)` → inject the inbound note (the transport writes it),
|
|
21
|
+
* - `persistFire(job)` → write back lastRunAt/lastStatus (the store PATCHes).
|
|
22
|
+
*
|
|
23
|
+
* `nextRunAt` is COMPUTED IN MEMORY and NEVER persisted. The runner keeps a small
|
|
24
|
+
* per-job horizon map (keyed by id) across ticks so a job fires once per slot; a
|
|
25
|
+
* job seen for the first time gets a horizon computed from now (and won't fire
|
|
26
|
+
* until that horizon passes — so a freshly-created job never back-fires on its
|
|
27
|
+
* first tick).
|
|
28
|
+
*
|
|
29
|
+
* Catch-up policy = FIRE-ONCE-ON-MISS: if a job's horizon is already in the past
|
|
30
|
+
* on a tick (the daemon was down across one or more slots), the job fires ONCE and
|
|
31
|
+
* the horizon is recomputed forward from now — never replaying every missed slot.
|
|
32
|
+
*
|
|
33
|
+
* Idempotency under overlap: a job that's mid-fire (its async fire hasn't resolved)
|
|
34
|
+
* is SKIPPED on subsequent ticks, so a slow vault write can't be double-fired.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { nextRunAfter } from "./cron.ts";
|
|
38
|
+
import type { Job } from "./jobs.ts";
|
|
39
|
+
|
|
40
|
+
/** Load the current jobs (the vault-native store queries the vault). Async. */
|
|
41
|
+
export type LoadJobsFn = () => Promise<Job[]>;
|
|
42
|
+
|
|
43
|
+
/** Fire a job: inject its message as an inbound note onto its channel. Async. */
|
|
44
|
+
export type FireFn = (job: Job) => Promise<void>;
|
|
45
|
+
|
|
46
|
+
/** Persist a job's bookkeeping (lastRunAt/lastStatus) after a fire. Async. */
|
|
47
|
+
export type PersistFireFn = (job: Job) => Promise<void>;
|
|
48
|
+
|
|
49
|
+
/** A scheduler driver: schedule `fn` to run every `ms`, return a cancel handle. */
|
|
50
|
+
export interface TickDriver {
|
|
51
|
+
schedule(fn: () => void, ms: number): { cancel: () => void };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface RunnerOptions {
|
|
55
|
+
/** Load the current jobs (queries the vault each tick). */
|
|
56
|
+
loadJobs: LoadJobsFn;
|
|
57
|
+
/** Fire a due job (inject the inbound note). */
|
|
58
|
+
fire: FireFn;
|
|
59
|
+
/** Persist a job's bookkeeping after a fire. */
|
|
60
|
+
persistFire: PersistFireFn;
|
|
61
|
+
/** Clock — injected for determinism. Default `() => new Date()`. */
|
|
62
|
+
now?: () => Date;
|
|
63
|
+
/** Tick driver — injected for determinism. Default a real-setInterval driver. */
|
|
64
|
+
driver?: TickDriver;
|
|
65
|
+
/** Tick interval (ms). Default 30s. */
|
|
66
|
+
intervalMs?: number;
|
|
67
|
+
/** Log sink (errors per job/tick never throw out). Default `console`. */
|
|
68
|
+
log?: { warn: (msg: string) => void; error: (msg: string) => void };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A real `setInterval`-backed tick driver (the daemon uses this; tests don't). */
|
|
72
|
+
export function realTickDriver(): TickDriver {
|
|
73
|
+
return {
|
|
74
|
+
schedule(fn, ms) {
|
|
75
|
+
const t = setInterval(fn, ms);
|
|
76
|
+
// Don't keep the process alive solely for the runner tick.
|
|
77
|
+
if (typeof t === "object" && t && "unref" in t) (t as { unref: () => void }).unref();
|
|
78
|
+
return { cancel: () => clearInterval(t) };
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const DEFAULT_INTERVAL_MS = 30_000;
|
|
84
|
+
|
|
85
|
+
export class Runner {
|
|
86
|
+
private readonly loadJobs: LoadJobsFn;
|
|
87
|
+
private readonly fire: FireFn;
|
|
88
|
+
private readonly persistFire: PersistFireFn;
|
|
89
|
+
private readonly now: () => Date;
|
|
90
|
+
private readonly driver: TickDriver;
|
|
91
|
+
private readonly intervalMs: number;
|
|
92
|
+
private readonly log: { warn: (msg: string) => void; error: (msg: string) => void };
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Per-job next-fire horizon (ISO), COMPUTED IN MEMORY and carried across ticks.
|
|
96
|
+
* Keyed by job id. Seeded the first time a job is seen; recomputed forward after
|
|
97
|
+
* each fire. Not persisted — the vault store doesn't carry nextRunAt.
|
|
98
|
+
*/
|
|
99
|
+
private readonly horizons = new Map<string, string>();
|
|
100
|
+
/** Job ids currently mid-fire — skipped by an interleaving tick (overlap guard). */
|
|
101
|
+
private readonly inFlight = new Set<string>();
|
|
102
|
+
private handle: { cancel: () => void } | undefined;
|
|
103
|
+
|
|
104
|
+
constructor(opts: RunnerOptions) {
|
|
105
|
+
this.loadJobs = opts.loadJobs;
|
|
106
|
+
this.fire = opts.fire;
|
|
107
|
+
this.persistFire = opts.persistFire;
|
|
108
|
+
this.now = opts.now ?? (() => new Date());
|
|
109
|
+
this.driver = opts.driver ?? realTickDriver();
|
|
110
|
+
this.intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
111
|
+
this.log = opts.log ?? {
|
|
112
|
+
warn: (m) => console.warn(m),
|
|
113
|
+
error: (m) => console.error(m),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The stable per-job key for horizon + in-flight tracking. Prefer the vault
|
|
119
|
+
* `noteId` (globally unique across channels/vaults) so two jobs that share a
|
|
120
|
+
* SLUG in different channels don't collide; fall back to the slug `id` for an
|
|
121
|
+
* in-memory job that hasn't been persisted yet (tests).
|
|
122
|
+
*/
|
|
123
|
+
private keyOf(job: Job): string {
|
|
124
|
+
return job.noteId ?? job.id;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Compute the next fire instant for a job after `from`, or null on a bad cron. */
|
|
128
|
+
private computeNext(job: Job, from: Date): Date | null {
|
|
129
|
+
try {
|
|
130
|
+
return nextRunAfter(job.schedule.cron, job.schedule.tz, from);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
this.log.warn(`runner: job "${job.id}" has an unschedulable cron: ${(err as Error).message}`);
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Start the periodic tick. Idempotent (a second start is a no-op). */
|
|
138
|
+
start(): void {
|
|
139
|
+
if (this.handle) return;
|
|
140
|
+
this.handle = this.driver.schedule(() => {
|
|
141
|
+
// A tick must never throw out (it'd kill the interval). `tick()` already
|
|
142
|
+
// guards; this is belt-and-suspenders for an unexpected throw.
|
|
143
|
+
void this.tick().catch((err) => this.log.error(`runner: tick failed: ${err}`));
|
|
144
|
+
}, this.intervalMs);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Stop the tick. Safe to call when not started. */
|
|
148
|
+
stop(): void {
|
|
149
|
+
this.handle?.cancel();
|
|
150
|
+
this.handle = undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* One scheduling pass. Loads the current jobs; for each enabled, not-in-flight
|
|
155
|
+
* job whose horizon is due (≤ now), fires it once and advances the horizon
|
|
156
|
+
* forward from now. A job seen for the first time gets a horizon computed (it
|
|
157
|
+
* won't fire this tick — it's in the future). Per-job failures are caught and
|
|
158
|
+
* recorded; one bad job never aborts the pass. A load failure is logged and the
|
|
159
|
+
* tick is a no-op (it retries next interval). Awaitable for deterministic tests.
|
|
160
|
+
*/
|
|
161
|
+
async tick(): Promise<void> {
|
|
162
|
+
const at = this.now();
|
|
163
|
+
let jobs: Job[];
|
|
164
|
+
try {
|
|
165
|
+
jobs = await this.loadJobs();
|
|
166
|
+
} catch (err) {
|
|
167
|
+
this.log.error(`runner: loadJobs failed (skipping this tick): ${(err as Error).message}`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Prune horizons for jobs that no longer exist (deleted), so the map can't grow.
|
|
172
|
+
const liveKeys = new Set(jobs.map((j) => this.keyOf(j)));
|
|
173
|
+
for (const key of [...this.horizons.keys()]) {
|
|
174
|
+
if (!liveKeys.has(key)) this.horizons.delete(key);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const fires: Array<Promise<void>> = [];
|
|
178
|
+
for (const job of jobs) {
|
|
179
|
+
if (!job.enabled) continue;
|
|
180
|
+
const key = this.keyOf(job);
|
|
181
|
+
if (this.inFlight.has(key)) continue; // overlap guard — already firing.
|
|
182
|
+
|
|
183
|
+
let horizon = this.horizons.get(key);
|
|
184
|
+
if (!horizon) {
|
|
185
|
+
// First time we've seen this job — seed a horizon from now. Future → not
|
|
186
|
+
// due this tick (a freshly-created job never back-fires on first sight).
|
|
187
|
+
const next = this.computeNext(job, at);
|
|
188
|
+
if (!next) continue; // unschedulable cron — skip (logged in computeNext).
|
|
189
|
+
horizon = next.toISOString();
|
|
190
|
+
this.horizons.set(key, horizon);
|
|
191
|
+
job.nextRunAt = horizon;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
job.nextRunAt = horizon;
|
|
196
|
+
if (new Date(horizon).getTime() <= at.getTime()) {
|
|
197
|
+
fires.push(this.fireOne(job, at));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
await Promise.allSettled(fires);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Fire one due job: mark in-flight, inject, record bookkeeping, recompute the
|
|
205
|
+
* horizon FORWARD FROM NOW (fire-once-on-miss — never replay missed slots),
|
|
206
|
+
* persist the bookkeeping. Never throws — a fire failure is recorded as
|
|
207
|
+
* `lastStatus: "error: …"` and still advances the horizon (so it retries the
|
|
208
|
+
* next slot rather than getting stuck).
|
|
209
|
+
*/
|
|
210
|
+
private async fireOne(job: Job, at: Date): Promise<void> {
|
|
211
|
+
const key = this.keyOf(job);
|
|
212
|
+
this.inFlight.add(key);
|
|
213
|
+
try {
|
|
214
|
+
await this.fire(job);
|
|
215
|
+
job.lastStatus = "ok";
|
|
216
|
+
} catch (err) {
|
|
217
|
+
job.lastStatus = `error: ${(err as Error).message}`;
|
|
218
|
+
this.log.error(`runner: job "${job.id}" fire failed: ${(err as Error).message}`);
|
|
219
|
+
} finally {
|
|
220
|
+
job.lastRunAt = at.toISOString();
|
|
221
|
+
// Recompute the horizon from NOW (not the missed slot) — fire-once-on-miss.
|
|
222
|
+
const next = this.computeNext(job, at);
|
|
223
|
+
if (next) {
|
|
224
|
+
this.horizons.set(key, next.toISOString());
|
|
225
|
+
job.nextRunAt = next.toISOString();
|
|
226
|
+
} else {
|
|
227
|
+
this.horizons.delete(key);
|
|
228
|
+
job.nextRunAt = undefined;
|
|
229
|
+
}
|
|
230
|
+
this.inFlight.delete(key);
|
|
231
|
+
// Persist the bookkeeping (lastRunAt/lastStatus) — best-effort.
|
|
232
|
+
try {
|
|
233
|
+
await this.persistFire(job);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
this.log.warn(`runner: persist bookkeeping for "${job.id}" failed (continuing): ${(err as Error).message}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Fire a single job immediately, on demand (the "Run now" API). Loads jobs to
|
|
242
|
+
* find it, bypasses the schedule + due check but honors the overlap guard and
|
|
243
|
+
* records bookkeeping exactly like a scheduled fire. Returns the resulting
|
|
244
|
+
* `lastStatus`. Throws only if the job is unknown; a fire failure is recorded
|
|
245
|
+
* and returned, not thrown.
|
|
246
|
+
*/
|
|
247
|
+
async runNow(id: string): Promise<string> {
|
|
248
|
+
const jobs = await this.loadJobs();
|
|
249
|
+
const job = jobs.find((j) => j.id === id);
|
|
250
|
+
if (!job) throw new Error(`runner: no job with id "${id}"`);
|
|
251
|
+
if (this.inFlight.has(this.keyOf(job))) return job.lastStatus ?? "already running";
|
|
252
|
+
await this.fireOne(job, this.now());
|
|
253
|
+
return job.lastStatus ?? "ok";
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { buildSandboxConfig } from "./config.ts";
|
|
3
|
+
import type { AgentSpec, BaseBinds } from "./types.ts";
|
|
4
|
+
import type { EgressBaseInput } from "./egress.ts";
|
|
5
|
+
|
|
6
|
+
const BASE_BINDS: BaseBinds = {
|
|
7
|
+
workspace: "/state/sessions/arm",
|
|
8
|
+
runtimeReadOnly: ["/home/op/.claude"],
|
|
9
|
+
};
|
|
10
|
+
const EGRESS_BASE: EgressBaseInput = { hubOrigin: "https://hub.example.com" };
|
|
11
|
+
|
|
12
|
+
// Most cases exercise the egress floor, which needs network "restricted". Scoped
|
|
13
|
+
// reads are the DEFAULT (filesystem "workspace"), so the helper only sets the
|
|
14
|
+
// network and leaves filesystem at its default. A spread `p` overrides (e.g.
|
|
15
|
+
// `filesystem: "full"` to test broad reads).
|
|
16
|
+
function specOf(p: Partial<AgentSpec> = {}): AgentSpec {
|
|
17
|
+
return { name: "arm", channels: ["ch"], network: "restricted", ...p };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("buildSandboxConfig — defaults (scoped reads + open network)", () => {
|
|
21
|
+
test("DEFAULT: scoped reads (home tree denied) + open network (no allowedDomains), writes confined", () => {
|
|
22
|
+
const cfg = buildSandboxConfig({
|
|
23
|
+
spec: { name: "arm", channels: ["ch"] }, // no filesystem/network → both defaults
|
|
24
|
+
baseBinds: BASE_BINDS,
|
|
25
|
+
egressBase: EGRESS_BASE,
|
|
26
|
+
platform: "darwin",
|
|
27
|
+
});
|
|
28
|
+
// Scoped reads by default: the home tree is DENIED — this is what keeps the
|
|
29
|
+
// operator's secrets (~/.parachute/operator.token, SSH keys) unreadable.
|
|
30
|
+
expect(cfg.filesystem.denyRead).toContain("/Users");
|
|
31
|
+
// Open network by default: allowedDomains omitted entirely (runtime = no restriction).
|
|
32
|
+
expect((cfg.network as { allowedDomains?: string[] }).allowedDomains).toBeUndefined();
|
|
33
|
+
// Writes confined to the workspace.
|
|
34
|
+
expect(cfg.filesystem.allowWrite).toContain("/state/sessions/arm");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("filesystem 'full': broad reads (no home-tree deny), writes still confined", () => {
|
|
38
|
+
const cfg = buildSandboxConfig({
|
|
39
|
+
spec: { name: "arm", channels: ["ch"], filesystem: "full" },
|
|
40
|
+
baseBinds: BASE_BINDS,
|
|
41
|
+
egressBase: EGRESS_BASE,
|
|
42
|
+
platform: "darwin",
|
|
43
|
+
});
|
|
44
|
+
expect(cfg.filesystem.denyRead).toEqual([]);
|
|
45
|
+
expect(cfg.filesystem.allowWrite).toContain("/state/sessions/arm");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("buildSandboxConfig — spec → SandboxRuntimeConfig", () => {
|
|
50
|
+
test("network: deny-by-default + base floor present, deniedDomains empty", () => {
|
|
51
|
+
const cfg = buildSandboxConfig({
|
|
52
|
+
spec: specOf({ egress: [] }),
|
|
53
|
+
baseBinds: BASE_BINDS,
|
|
54
|
+
egressBase: EGRESS_BASE,
|
|
55
|
+
platform: "darwin",
|
|
56
|
+
});
|
|
57
|
+
expect(cfg.network.allowedDomains).toContain("api.anthropic.com");
|
|
58
|
+
expect(cfg.network.allowedDomains).toContain("hub.example.com");
|
|
59
|
+
expect(cfg.network.deniedDomains).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("SECURITY: a spec with foreign egress still carries the base floor", () => {
|
|
63
|
+
const cfg = buildSandboxConfig({
|
|
64
|
+
spec: specOf({ egress: ["registry.npmjs.org"] }),
|
|
65
|
+
baseBinds: BASE_BINDS,
|
|
66
|
+
egressBase: EGRESS_BASE,
|
|
67
|
+
platform: "darwin",
|
|
68
|
+
});
|
|
69
|
+
expect(cfg.network.allowedDomains).toContain("api.anthropic.com");
|
|
70
|
+
expect(cfg.network.allowedDomains).toContain("hub.example.com");
|
|
71
|
+
expect(cfg.network.allowedDomains).toContain("registry.npmjs.org");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("filesystem: scoped reads (deny home tree, re-allow binds) + write confinement", () => {
|
|
75
|
+
const cfg = buildSandboxConfig({
|
|
76
|
+
spec: specOf({ mounts: [{ hostPath: "/proj", mountPath: "/work", mode: "rw" }] }),
|
|
77
|
+
baseBinds: BASE_BINDS,
|
|
78
|
+
egressBase: EGRESS_BASE,
|
|
79
|
+
platform: "darwin",
|
|
80
|
+
});
|
|
81
|
+
expect(cfg.filesystem.denyRead).toContain("/Users");
|
|
82
|
+
expect(cfg.filesystem.allowRead).toContain("/state/sessions/arm");
|
|
83
|
+
expect(cfg.filesystem.allowRead).toContain("/home/op/.claude");
|
|
84
|
+
expect(cfg.filesystem.allowRead).toContain("/proj");
|
|
85
|
+
expect(cfg.filesystem.allowWrite).toContain("/state/sessions/arm");
|
|
86
|
+
expect(cfg.filesystem.allowWrite).toContain("/proj");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("Linux platform denies /home instead of /Users", () => {
|
|
90
|
+
const cfg = buildSandboxConfig({
|
|
91
|
+
spec: specOf(),
|
|
92
|
+
baseBinds: BASE_BINDS,
|
|
93
|
+
egressBase: EGRESS_BASE,
|
|
94
|
+
platform: "linux",
|
|
95
|
+
});
|
|
96
|
+
expect(cfg.filesystem.denyRead).toContain("/home");
|
|
97
|
+
expect(cfg.filesystem.denyRead).not.toContain("/Users");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("allowPty defaults true (interactive claude needs a pty)", () => {
|
|
101
|
+
const cfg = buildSandboxConfig({
|
|
102
|
+
spec: specOf(),
|
|
103
|
+
baseBinds: BASE_BINDS,
|
|
104
|
+
egressBase: EGRESS_BASE,
|
|
105
|
+
platform: "darwin",
|
|
106
|
+
});
|
|
107
|
+
expect(cfg.allowPty).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("ripgrep override threads through when provided", () => {
|
|
111
|
+
const cfg = buildSandboxConfig({
|
|
112
|
+
spec: specOf(),
|
|
113
|
+
baseBinds: BASE_BINDS,
|
|
114
|
+
egressBase: EGRESS_BASE,
|
|
115
|
+
platform: "darwin",
|
|
116
|
+
ripgrep: { command: "/abs/rg" },
|
|
117
|
+
});
|
|
118
|
+
expect(cfg.ripgrep).toEqual({ command: "/abs/rg" });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("a restricted-network config carries the full runtime shape (allowedDomains present)", () => {
|
|
122
|
+
const cfg = buildSandboxConfig({
|
|
123
|
+
spec: specOf(), // network "restricted" → allowedDomains present
|
|
124
|
+
baseBinds: BASE_BINDS,
|
|
125
|
+
egressBase: EGRESS_BASE,
|
|
126
|
+
platform: "darwin",
|
|
127
|
+
});
|
|
128
|
+
expect(cfg.network).toHaveProperty("allowedDomains");
|
|
129
|
+
expect(cfg.network).toHaveProperty("deniedDomains");
|
|
130
|
+
expect(cfg.filesystem).toHaveProperty("denyRead");
|
|
131
|
+
expect(cfg.filesystem).toHaveProperty("allowRead");
|
|
132
|
+
expect(cfg.filesystem).toHaveProperty("allowWrite");
|
|
133
|
+
expect(cfg.filesystem).toHaveProperty("denyWrite");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("an open-network config OMITS allowedDomains but keeps the rest of the shape", () => {
|
|
137
|
+
const cfg = buildSandboxConfig({
|
|
138
|
+
spec: { name: "arm", channels: ["ch"] }, // default → network open
|
|
139
|
+
baseBinds: BASE_BINDS,
|
|
140
|
+
egressBase: EGRESS_BASE,
|
|
141
|
+
platform: "darwin",
|
|
142
|
+
});
|
|
143
|
+
// allowedDomains is deliberately ABSENT on open (the runtime's allow-all shape);
|
|
144
|
+
// this is NOT a protocol guarantee that allowedDomains is always present.
|
|
145
|
+
expect(cfg.network).not.toHaveProperty("allowedDomains");
|
|
146
|
+
expect(cfg.network).toHaveProperty("deniedDomains");
|
|
147
|
+
expect(cfg.filesystem).toHaveProperty("denyRead");
|
|
148
|
+
expect(cfg.filesystem).toHaveProperty("allowWrite");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map an agent-spec → a `SandboxRuntimeConfig` for `@anthropic-ai/sandbox-runtime`
|
|
3
|
+
* (design §3 — the isolation envelope).
|
|
4
|
+
*
|
|
5
|
+
* The runtime config shape (verified against the package's own types,
|
|
6
|
+
* `@anthropic-ai/sandbox-runtime` 0.0.54 `SandboxRuntimeConfig`):
|
|
7
|
+
*
|
|
8
|
+
* {
|
|
9
|
+
* network: { allowedDomains: string[], deniedDomains: string[] },
|
|
10
|
+
* filesystem: { denyRead: string[], allowRead?: string[],
|
|
11
|
+
* allowWrite: string[], denyWrite: string[] },
|
|
12
|
+
* …optional knobs (allowPty, bwrapPath, ripgrep, …)
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* This module is the single place spec → runtime-config happens, so the egress
|
|
16
|
+
* floor (§4.4) and scoped-read policy (§4.5) are guaranteed on every launch.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { SandboxRuntimeConfig } from "@anthropic-ai/sandbox-runtime";
|
|
20
|
+
import type { AgentSpec, BaseBinds, SandboxPlatform } from "./types.ts";
|
|
21
|
+
import { composeEgressAllowlist, type EgressBaseInput } from "./egress.ts";
|
|
22
|
+
import { composeFilesystemView } from "./mounts.ts";
|
|
23
|
+
|
|
24
|
+
export interface BuildSandboxConfigInput {
|
|
25
|
+
spec: AgentSpec;
|
|
26
|
+
/** Workspace + runtime/config binds the contract always grants. */
|
|
27
|
+
baseBinds: BaseBinds;
|
|
28
|
+
/** Origins for the non-removable egress base. */
|
|
29
|
+
egressBase: EgressBaseInput;
|
|
30
|
+
/** Target platform. Defaults to the running platform. */
|
|
31
|
+
platform?: SandboxPlatform;
|
|
32
|
+
/**
|
|
33
|
+
* Allow the session to allocate a pty (a tmux/interactive `claude` needs one).
|
|
34
|
+
* Defaults true. Surfaced so a non-interactive arm can drop it.
|
|
35
|
+
*/
|
|
36
|
+
allowPty?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Optional ripgrep override the runtime uses for deny-path scanning. On macOS
|
|
39
|
+
* the runtime needs a ripgrep binary; pass `{ command: <abs path> }` when the
|
|
40
|
+
* host has no `rg` on PATH. Omitted = the runtime's own resolution.
|
|
41
|
+
*/
|
|
42
|
+
ripgrep?: { command: string; args?: string[] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Resolve the running platform to the two we support. */
|
|
46
|
+
export function currentSandboxPlatform(): SandboxPlatform {
|
|
47
|
+
return process.platform === "darwin" ? "darwin" : "linux";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build the `SandboxRuntimeConfig` for an agent spec. The egress allowlist is the
|
|
52
|
+
* non-removable base unioned with the spec's additions; the filesystem view is
|
|
53
|
+
* scoped-read (home-tree denied, binds re-allowed) with writes confined to the
|
|
54
|
+
* workspace + rw mounts.
|
|
55
|
+
*/
|
|
56
|
+
export function buildSandboxConfig(input: BuildSandboxConfigInput): SandboxRuntimeConfig {
|
|
57
|
+
const platform = input.platform ?? currentSandboxPlatform();
|
|
58
|
+
|
|
59
|
+
// The two containment boundaries are INDEPENDENT (Anthropic's model). Each has a
|
|
60
|
+
// security-first default: reads scoped to the workspace, network open.
|
|
61
|
+
const scopedReads = input.spec.filesystem !== "full"; // default "workspace" = scoped
|
|
62
|
+
const restrictedNet = input.spec.network === "restricted"; // default "open"
|
|
63
|
+
|
|
64
|
+
// Filesystem: scoped reads (deny home tree, re-allow private home + claude
|
|
65
|
+
// runtime + the working dir + mounts) unless explicitly "full". Writes are
|
|
66
|
+
// confined to the private home + the working dir + rw mounts in BOTH cases. The
|
|
67
|
+
// scoped default is what keeps the operator's secrets (e.g.
|
|
68
|
+
// ~/.parachute/operator.token) unreadable even with the network open. The spec's
|
|
69
|
+
// `workspace` (the shared real dir the agent works from) is threaded in as an rw
|
|
70
|
+
// working-root — decoupled from the private home, so `.mcp.json`'s secrets stay
|
|
71
|
+
// per-agent even when the working dir is shared (design
|
|
72
|
+
// 2026-06-16-agent-filesystem-and-sharing.md).
|
|
73
|
+
const fs = composeFilesystemView(
|
|
74
|
+
input.baseBinds,
|
|
75
|
+
input.spec.mounts,
|
|
76
|
+
platform,
|
|
77
|
+
scopedReads,
|
|
78
|
+
input.spec.workspace,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Network: "open" (default) OMITS `allowedDomains` — the runtime treats an absent
|
|
82
|
+
// allowedDomains as "no restriction" (verified: present-but-empty = block all,
|
|
83
|
+
// absent = allow all). "restricted" = the non-removable base UNIONed with the
|
|
84
|
+
// spec's additions. The cast is because the runtime's TS type marks allowedDomains
|
|
85
|
+
// required while its runtime honors the absent case as the documented allow-all.
|
|
86
|
+
const network: SandboxRuntimeConfig["network"] = restrictedNet
|
|
87
|
+
? { allowedDomains: composeEgressAllowlist(input.egressBase, input.spec.egress), deniedDomains: [] }
|
|
88
|
+
: ({ deniedDomains: [] } as unknown as SandboxRuntimeConfig["network"]);
|
|
89
|
+
|
|
90
|
+
const config: SandboxRuntimeConfig = {
|
|
91
|
+
network,
|
|
92
|
+
filesystem: {
|
|
93
|
+
denyRead: fs.denyRead,
|
|
94
|
+
allowRead: fs.allowRead,
|
|
95
|
+
allowWrite: fs.allowWrite,
|
|
96
|
+
denyWrite: fs.denyWrite,
|
|
97
|
+
},
|
|
98
|
+
allowPty: input.allowPty ?? true,
|
|
99
|
+
};
|
|
100
|
+
if (input.ripgrep) config.ripgrep = input.ripgrep;
|
|
101
|
+
return config;
|
|
102
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
ANTHROPIC_EGRESS_HOSTS,
|
|
4
|
+
baseEgressAllowlist,
|
|
5
|
+
composeEgressAllowlist,
|
|
6
|
+
hostFromOrigin,
|
|
7
|
+
type EgressBaseInput,
|
|
8
|
+
} from "./egress.ts";
|
|
9
|
+
|
|
10
|
+
const BASE: EgressBaseInput = { hubOrigin: "https://hub.example.com" };
|
|
11
|
+
|
|
12
|
+
describe("hostFromOrigin", () => {
|
|
13
|
+
test("reduces an origin to its hostname (strips scheme + port + path)", () => {
|
|
14
|
+
expect(hostFromOrigin("https://hub.example.com:1939/admin")).toBe("hub.example.com");
|
|
15
|
+
});
|
|
16
|
+
test("passes a bare host through", () => {
|
|
17
|
+
expect(hostFromOrigin("registry.npmjs.org")).toBe("registry.npmjs.org");
|
|
18
|
+
});
|
|
19
|
+
test("strips a :port from a bare host:port", () => {
|
|
20
|
+
expect(hostFromOrigin("127.0.0.1:1939")).toBe("127.0.0.1");
|
|
21
|
+
});
|
|
22
|
+
test("preserves loopback (a co-located dev hub is loopback)", () => {
|
|
23
|
+
expect(hostFromOrigin("http://127.0.0.1:1939")).toBe("127.0.0.1");
|
|
24
|
+
});
|
|
25
|
+
test("returns null for empty / nullish input", () => {
|
|
26
|
+
expect(hostFromOrigin("")).toBeNull();
|
|
27
|
+
expect(hostFromOrigin(undefined)).toBeNull();
|
|
28
|
+
expect(hostFromOrigin(" ")).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("baseEgressAllowlist — the non-removable base", () => {
|
|
33
|
+
test("always includes the Anthropic hosts + the hub host", () => {
|
|
34
|
+
const base = baseEgressAllowlist(BASE);
|
|
35
|
+
for (const h of ANTHROPIC_EGRESS_HOSTS) expect(base).toContain(h);
|
|
36
|
+
expect(base).toContain("hub.example.com");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("includes a distinct vault host when given", () => {
|
|
40
|
+
const base = baseEgressAllowlist({ ...BASE, vaultOrigin: "https://vault.example.com" });
|
|
41
|
+
expect(base).toContain("vault.example.com");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("dedupes a vault origin equal to the hub origin", () => {
|
|
45
|
+
const base = baseEgressAllowlist({
|
|
46
|
+
hubOrigin: "https://h.example.com",
|
|
47
|
+
vaultOrigin: "https://h.example.com",
|
|
48
|
+
});
|
|
49
|
+
expect(base.filter((h) => h === "h.example.com")).toHaveLength(1);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("composeEgressAllowlist — base floor is non-removable, spec is additive", () => {
|
|
54
|
+
test("an empty spec egress still gets the full base (weaver-style arm)", () => {
|
|
55
|
+
const allow = composeEgressAllowlist(BASE, []);
|
|
56
|
+
for (const h of ANTHROPIC_EGRESS_HOSTS) expect(allow).toContain(h);
|
|
57
|
+
expect(allow).toContain("hub.example.com");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("an undefined spec egress still gets the full base", () => {
|
|
61
|
+
const allow = composeEgressAllowlist(BASE, undefined);
|
|
62
|
+
expect(allow).toContain("api.anthropic.com");
|
|
63
|
+
expect(allow).toContain("hub.example.com");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("spec hosts are ADDED on top of the base", () => {
|
|
67
|
+
const allow = composeEgressAllowlist(BASE, ["registry.npmjs.org", "pypi.org"]);
|
|
68
|
+
// base present...
|
|
69
|
+
expect(allow).toContain("api.anthropic.com");
|
|
70
|
+
expect(allow).toContain("hub.example.com");
|
|
71
|
+
// ...plus the additions
|
|
72
|
+
expect(allow).toContain("registry.npmjs.org");
|
|
73
|
+
expect(allow).toContain("pypi.org");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("SECURITY: a spec that lists ONLY a foreign host CANNOT drop the base — the base is still present", () => {
|
|
77
|
+
// A spec authored to omit the base entirely (the malicious-omit case).
|
|
78
|
+
const allow = composeEgressAllowlist(BASE, ["evil.example.com"]);
|
|
79
|
+
// The base floor survives regardless of what the spec listed.
|
|
80
|
+
expect(allow).toContain("api.anthropic.com");
|
|
81
|
+
expect(allow).toContain("hub.example.com");
|
|
82
|
+
// The spec's own host is added (additive), not a replacement.
|
|
83
|
+
expect(allow).toContain("evil.example.com");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("SECURITY: a spec cannot REPLACE the Anthropic host with a look-alike — both end up present, base is not dropped", () => {
|
|
87
|
+
// A spec that tries to "override" the Anthropic host by re-declaring a near-miss.
|
|
88
|
+
const allow = composeEgressAllowlist(BASE, ["api.anthropic.com.evil.example.com"]);
|
|
89
|
+
// The real Anthropic apex is still on the list (the base recomputed from code).
|
|
90
|
+
expect(allow).toContain("api.anthropic.com");
|
|
91
|
+
// The look-alike is just an additional (separate) host, not a substitution.
|
|
92
|
+
expect(allow).toContain("api.anthropic.com.evil.example.com");
|
|
93
|
+
// And the look-alike did not evict the real host.
|
|
94
|
+
expect(allow.indexOf("api.anthropic.com")).toBeGreaterThanOrEqual(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("the base always sorts FIRST (recomputed from code, prepended)", () => {
|
|
98
|
+
const allow = composeEgressAllowlist(BASE, ["z-late.example.com"]);
|
|
99
|
+
expect(allow[0]).toBe("api.anthropic.com");
|
|
100
|
+
expect(allow[allow.length - 1]).toBe("z-late.example.com");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("spec origins are normalized to hosts (full URL and bare host land the same)", () => {
|
|
104
|
+
const a = composeEgressAllowlist(BASE, ["https://registry.npmjs.org"]);
|
|
105
|
+
const b = composeEgressAllowlist(BASE, ["registry.npmjs.org"]);
|
|
106
|
+
expect(a).toEqual(b);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("dedupes a spec host that duplicates a base host", () => {
|
|
110
|
+
const allow = composeEgressAllowlist(BASE, ["hub.example.com"]);
|
|
111
|
+
expect(allow.filter((h) => h === "hub.example.com")).toHaveLength(1);
|
|
112
|
+
});
|
|
113
|
+
});
|