saeeol 1.3.0 → 1.4.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/AGENTS.md +72 -0
- package/BUN_SHELL_MIGRATION_PLAN.md +136 -0
- package/Dockerfile +18 -0
- package/assets/saeeol.ico +0 -0
- package/bin/saeeol.cjs +3 -1
- package/bunfig.toml +7 -0
- package/database.db +0 -0
- package/drizzle.config.ts +10 -0
- package/git +0 -0
- package/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
- package/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
- package/migration/20260211171708_add_project_commands/migration.sql +1 -0
- package/migration/20260211171708_add_project_commands/snapshot.json +806 -0
- package/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
- package/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
- package/migration/20260225215848_workspace/migration.sql +7 -0
- package/migration/20260225215848_workspace/snapshot.json +959 -0
- package/migration/20260227213759_add_session_workspace_id/migration.sql +2 -0
- package/migration/20260227213759_add_session_workspace_id/snapshot.json +983 -0
- package/migration/20260228203230_blue_harpoon/migration.sql +17 -0
- package/migration/20260228203230_blue_harpoon/snapshot.json +1102 -0
- package/migration/20260303231226_add_workspace_fields/migration.sql +5 -0
- package/migration/20260303231226_add_workspace_fields/snapshot.json +1013 -0
- package/migration/20260309230000_move_org_to_state/migration.sql +3 -0
- package/migration/20260309230000_move_org_to_state/snapshot.json +1156 -0
- package/migration/20260312043431_session_message_cursor/migration.sql +4 -0
- package/migration/20260312043431_session_message_cursor/snapshot.json +1168 -0
- package/migration/20260323234822_events/migration.sql +13 -0
- package/migration/20260323234822_events/snapshot.json +1271 -0
- package/migration/20260410174513_workspace-name/migration.sql +16 -0
- package/migration/20260410174513_workspace-name/snapshot.json +1271 -0
- package/migration/20260413175956_chief_energizer/migration.sql +13 -0
- package/migration/20260413175956_chief_energizer/snapshot.json +1399 -0
- package/migration/20260423070820_add_icon_url_override/migration.sql +2 -0
- package/migration/20260423070820_add_icon_url_override/snapshot.json +1409 -0
- package/migration/20260428004200_add_session_path/migration.sql +1 -0
- package/migration/20260428004200_add_session_path/snapshot.json +1419 -0
- package/npm/bin/saeeol +42 -0
- package/npm/package.json +39 -0
- package/npm/postinstall.js +162 -0
- package/package.json +201 -207
- package/parsers-config.ts +289 -0
- package/script/build.ts +393 -0
- package/script/check-migrations.ts +16 -0
- package/script/fix-node-pty.ts +34 -0
- package/script/generate.ts +23 -0
- package/script/postinstall.mjs +189 -0
- package/script/publish.ts +200 -0
- package/script/run-workspace-server +106 -0
- package/script/schema.ts +63 -0
- package/script/test-runner.ts +420 -0
- package/script/time.ts +6 -0
- package/script/trace-imports.ts +153 -0
- package/script/upgrade-opentui.ts +64 -0
- package/scripts/diff-sdk-types.sh +52 -0
- package/specs/effect/facades.md +221 -0
- package/specs/effect/http-api.md +401 -0
- package/specs/effect/instance-context.md +309 -0
- package/specs/effect/loose-ends.md +34 -0
- package/specs/effect/migration.md +299 -0
- package/specs/effect/routes.md +64 -0
- package/specs/effect/schema.md +399 -0
- package/specs/effect/server-package.md +668 -0
- package/specs/effect/tools.md +90 -0
- package/specs/tui-plugins.md +433 -0
- package/specs/v2/api.ts +67 -0
- package/specs/v2/keymappings.md +10 -0
- package/specs/v2/message-shape.md +136 -0
- package/src/acp/agent-message.ts +1 -1
- package/src/acp/agent-utils.ts +1 -1
- package/src/boxes/ansi.ts +17 -0
- package/src/boxes/atomic-write.ts +35 -0
- package/src/boxes/b64.ts +58 -0
- package/src/boxes/bash-security.ts +129 -0
- package/src/boxes/bom.ts +18 -0
- package/src/boxes/cancel.ts +16 -0
- package/src/boxes/chop.ts +12 -0
- package/src/boxes/clamp.ts +3 -0
- package/src/boxes/compact.ts +9 -0
- package/src/boxes/cost-tracker.ts +116 -0
- package/src/boxes/dataurl.ts +29 -0
- package/src/boxes/delay.ts +27 -0
- package/src/boxes/diff-apply.ts +53 -0
- package/src/boxes/disposable.ts +13 -0
- package/src/boxes/err.ts +34 -0
- package/src/boxes/human.ts +47 -0
- package/src/boxes/iife.ts +9 -0
- package/src/boxes/latch.ts +8 -0
- package/src/boxes/memory.ts +198 -0
- package/src/boxes/net.ts +16 -0
- package/src/boxes/plural.ts +4 -0
- package/src/boxes/puny.ts +21 -0
- package/src/boxes/retry.ts +49 -0
- package/src/boxes/rwlock.ts +41 -0
- package/src/boxes/schedule.ts +71 -0
- package/src/boxes/scope.ts +21 -0
- package/src/boxes/tokens.ts +9 -0
- package/src/boxes/ttl-cache.ts +63 -0
- package/src/boxes/typed-event.ts +51 -0
- package/src/boxes/uid.ts +50 -0
- package/src/boxes/wave6.test.ts +296 -0
- package/src/boxes/wildcard.ts +58 -0
- package/src/bus/global.ts +1 -1
- package/src/cli/cmd/github-run-api.ts +2 -2
- package/src/cli/cmd/run-events.ts +2 -2
- package/src/cli/cmd/tui/component/logo.tsx +1 -1
- package/src/cli/cmd/tui/component/prompt/use-prompt-memos.ts +2 -2
- package/src/cli/cmd/tui/context/app/editor-zed.ts +1 -1
- package/src/cli/cmd/tui/context/app/editor.ts +1 -1
- package/src/cli/cmd/tui/context/app/theme.tsx +1 -1
- package/src/cli/cmd/tui/preflight.ts +138 -0
- package/src/cli/cmd/tui/thread.ts +20 -0
- package/src/cli/cmd/tui/util/revert-diff.ts +1 -1
- package/src/overlay/cli/cmd/roll-call-call.ts +1 -1
- package/src/overlay/cost-tracker/format.ts +1 -1
- package/src/overlay/cost-tracker/index.ts +4 -4
- package/src/overlay/cost-tracker/state.ts +2 -2
- package/src/overlay/cost-tracker/types.ts +2 -2
- package/src/overlay/memory/age.ts +1 -1
- package/src/overlay/memory/index.ts +4 -4
- package/src/overlay/memory/paths.ts +2 -2
- package/src/overlay/memory/scan.ts +1 -1
- package/src/overlay/memory/types.ts +2 -2
- package/src/overlay/tool/bash-security.ts +3 -3
- package/src/overlay/util/url.ts +1 -1
- package/src/plugin/codex-auth.ts +1 -1
- package/src/provider/model-cache.ts +2 -2
- package/src/provider/provider-resolve.ts +3 -3
- package/src/provider/transform-message.ts +1 -1
- package/src/server/routes/game.ts +284 -0
- package/src/server/server.ts +2 -0
- package/src/session/core/compaction/compaction-helpers.ts +1 -1
- package/src/session/core/compaction/compaction.ts +1 -1
- package/src/session/core/session.ts +2 -0
- package/src/sessions/ingest-queue.ts +2 -2
- package/src/sessions/remote-ws.ts +1 -1
- package/src/tool/workflow/question.ts +1 -1
- package/src/util/abort.ts +1 -1
- package/src/util/bom.ts +2 -2
- package/src/util/color.ts +1 -1
- package/src/util/data-url.ts +1 -1
- package/src/util/defer.ts +1 -1
- package/src/util/error.ts +2 -2
- package/src/util/filesystem.ts +2 -2
- package/src/util/format.ts +1 -1
- package/src/util/iife.ts +1 -1
- package/src/util/local-context.ts +1 -1
- package/src/util/locale.ts +2 -2
- package/src/util/lock.ts +1 -1
- package/src/util/network.ts +1 -1
- package/src/util/signal.ts +1 -1
- package/src/util/token.ts +1 -1
- package/src/util/wildcard.ts +1 -1
- package/sst-env.d.ts +10 -0
- package/test/AGENTS.md +133 -0
- package/test/account/repo.test.ts +352 -0
- package/test/account/service.test.ts +456 -0
- package/test/acp/agent-interface.test.ts +51 -0
- package/test/acp/event-subscription.test.ts +725 -0
- package/test/agent/agent.test.ts +890 -0
- package/test/auth/auth.test.ts +86 -0
- package/test/bun/registry.test.ts +75 -0
- package/test/bus/bus-effect.test.ts +161 -0
- package/test/bus/bus-integration.test.ts +87 -0
- package/test/bus/bus.test.ts +219 -0
- package/test/cli/account.test.ts +26 -0
- package/test/cli/auto-mode.test.ts +75 -0
- package/test/cli/bin-saeeol.test.ts +8 -0
- package/test/cli/cmd/tui/prompt-part.test.ts +47 -0
- package/test/cli/cmd/tui/prompt-traits.test.ts +38 -0
- package/test/cli/cmd/tui/sync.test.tsx +159 -0
- package/test/cli/error.test.ts +18 -0
- package/test/cli/github-action.test.ts +198 -0
- package/test/cli/github-remote.test.ts +85 -0
- package/test/cli/import.test.ts +97 -0
- package/test/cli/install-artifact.test.ts +72 -0
- package/test/cli/plugin-auth-picker.test.ts +120 -0
- package/test/cli/pr.test.ts +59 -0
- package/test/cli/tui/editor-context-zed.test.ts +356 -0
- package/test/cli/tui/editor-context.test.tsx +228 -0
- package/test/cli/tui/keybind-plugin.test.ts +90 -0
- package/test/cli/tui/markdown.test.ts +161 -0
- package/test/cli/tui/plugin-add.test.ts +111 -0
- package/test/cli/tui/plugin-install.test.ts +87 -0
- package/test/cli/tui/plugin-lifecycle.test.ts +224 -0
- package/test/cli/tui/plugin-loader-entrypoint.test.ts +484 -0
- package/test/cli/tui/plugin-loader-pure.test.ts +71 -0
- package/test/cli/tui/plugin-loader.test.ts +816 -0
- package/test/cli/tui/plugin-toggle.test.ts +157 -0
- package/test/cli/tui/revert-diff.test.ts +35 -0
- package/test/cli/tui/slot-replace.test.tsx +47 -0
- package/test/cli/tui/theme-store.test.ts +54 -0
- package/test/cli/tui/thread.test.ts +28 -0
- package/test/cli/tui/transcript.test.ts +426 -0
- package/test/cli/tui/usage.test.ts +60 -0
- package/test/cli/tui/use-event.test.tsx +175 -0
- package/test/config/agent-color.test.ts +67 -0
- package/test/config/config.test.ts +2544 -0
- package/test/config/fixtures/empty-frontmatter.md +4 -0
- package/test/config/fixtures/frontmatter.md +28 -0
- package/test/config/fixtures/markdown-header.md +11 -0
- package/test/config/fixtures/no-frontmatter.md +1 -0
- package/test/config/fixtures/weird-model-id.md +13 -0
- package/test/config/lsp.test.ts +87 -0
- package/test/config/markdown.test.ts +228 -0
- package/test/config/plugin.test.ts +0 -0
- package/test/config/tui.test.ts +624 -0
- package/test/control-plane/adapters.test.ts +71 -0
- package/test/control-plane/workspace.test.ts +1526 -0
- package/test/effect/app-runtime-logger.test.ts +98 -0
- package/test/effect/config-service.test.ts +65 -0
- package/test/effect/instance-state.test.ts +394 -0
- package/test/effect/run-service.test.ts +89 -0
- package/test/effect/runner.test.ts +523 -0
- package/test/fake/provider.ts +82 -0
- package/test/file/fsmonitor.test.ts +68 -0
- package/test/file/ignore.test.ts +10 -0
- package/test/file/index.test.ts +954 -0
- package/test/file/path-traversal.test.ts +205 -0
- package/test/file/ripgrep.test.ts +226 -0
- package/test/file/watcher.test.ts +249 -0
- package/test/filesystem/filesystem.test.ts +319 -0
- package/test/fixture/db.ts +11 -0
- package/test/fixture/fixture.test.ts +26 -0
- package/test/fixture/fixture.ts +175 -0
- package/test/fixture/flock-worker.ts +72 -0
- package/test/fixture/log-init-worker.ts +62 -0
- package/test/fixture/lsp/fake-lsp-server.js +249 -0
- package/test/fixture/plug-worker.ts +93 -0
- package/test/fixture/plugin-meta-worker.ts +19 -0
- package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
- package/test/fixture/skills/cloudflare/SKILL.md +211 -0
- package/test/fixture/skills/index.json +6 -0
- package/test/fixture/tui-plugin.ts +323 -0
- package/test/fixture/tui-runtime.ts +31 -0
- package/test/format/format.test.ts +272 -0
- package/test/git/git.test.ts +128 -0
- package/test/ide/ide.test.ts +82 -0
- package/test/installation/installation.test.ts +168 -0
- package/test/keybind.test.ts +421 -0
- package/test/lib/effect.ts +53 -0
- package/test/lib/filesystem.ts +10 -0
- package/test/lib/llm-server.ts +778 -0
- package/test/lib/websocket.ts +46 -0
- package/test/lsp/client.test.ts +482 -0
- package/test/lsp/index.test.ts +160 -0
- package/test/lsp/launch.test.ts +22 -0
- package/test/lsp/lifecycle.test.ts +184 -0
- package/test/ltm/ltm.test.ts +230 -0
- package/test/mcp/headers.test.ts +178 -0
- package/test/mcp/lifecycle.test.ts +787 -0
- package/test/mcp/oauth-auto-connect.test.ts +311 -0
- package/test/mcp/oauth-browser.test.ts +276 -0
- package/test/mcp/oauth-callback.test.ts +34 -0
- package/test/memory/abort-leak-webfetch.ts +49 -0
- package/test/memory/abort-leak.test.ts +128 -0
- package/test/patch/patch.test.ts +348 -0
- package/test/permission/arity.test.ts +33 -0
- package/test/permission/next.test.ts +1227 -0
- package/test/permission/next.toConfig.test.ts +110 -0
- package/test/permission-task.test.ts +326 -0
- package/test/plugin/auth-override.test.ts +79 -0
- package/test/plugin/cloudflare.test.ts +68 -0
- package/test/plugin/codex.test.ts +123 -0
- package/test/plugin/github-copilot-models.test.ts +261 -0
- package/test/plugin/install-concurrency.test.ts +140 -0
- package/test/plugin/install.test.ts +570 -0
- package/test/plugin/loader-shared.test.ts +1169 -0
- package/test/plugin/meta.test.ts +137 -0
- package/test/plugin/plugin-contract.test.ts +291 -0
- package/test/plugin/shared.test.ts +88 -0
- package/test/plugin/trigger.test.ts +102 -0
- package/test/plugin/workspace-adapter.test.ts +109 -0
- package/test/preload.ts +77 -0
- package/test/project/instance.test.ts +276 -0
- package/test/project/migrate-global.test.ts +152 -0
- package/test/project/project.test.ts +600 -0
- package/test/project/vcs.test.ts +286 -0
- package/test/project/worktree-remove.test.ts +126 -0
- package/test/project/worktree.test.ts +223 -0
- package/test/provider/amazon-bedrock.test.ts +462 -0
- package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
- package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
- package/test/provider/gitlab-duo.test.ts +413 -0
- package/test/provider/local.test.ts +208 -0
- package/test/provider/models.test.ts +261 -0
- package/test/provider/provider-category.test.ts +190 -0
- package/test/provider/provider.test.ts +2758 -0
- package/test/provider/transform.test.ts +3681 -0
- package/test/pty/pty-output-isolation.test.ts +147 -0
- package/test/pty/pty-session.test.ts +102 -0
- package/test/pty/pty-shell.test.ts +104 -0
- package/test/question/question.test.ts +490 -0
- package/test/saeeol/agent-global-config-dirs.test.ts +24 -0
- package/test/saeeol/agent-manager-tool.test.ts +71 -0
- package/test/saeeol/agent-permission-overrides.test.ts +75 -0
- package/test/saeeol/agent-skill-permissions.test.ts +37 -0
- package/test/saeeol/ask-agent-permissions.test.ts +303 -0
- package/test/saeeol/bash-hierarchy.test.ts +64 -0
- package/test/saeeol/bash-permission-metadata.test.ts +66 -0
- package/test/saeeol/bash-security-extended.test.ts +243 -0
- package/test/saeeol/bedrock-claude-empty-content.test.ts +138 -0
- package/test/saeeol/boxes-integration.test.ts +415 -0
- package/test/saeeol/builtin-skills.test.ts +75 -0
- package/test/saeeol/cleanup.ts +28 -0
- package/test/saeeol/cli/dev-setup.test.ts +74 -0
- package/test/saeeol/cli/roll-call.test.ts +161 -0
- package/test/saeeol/cli-run-auto-helper.test.ts +58 -0
- package/test/saeeol/codex-auth-refresh.test.ts +124 -0
- package/test/saeeol/commit-message/generate.test.ts +188 -0
- package/test/saeeol/commit-message/git-context.test.ts +303 -0
- package/test/saeeol/commit-message-windows.test.ts +38 -0
- package/test/saeeol/compaction-payload-recovery.test.ts +406 -0
- package/test/saeeol/compaction-preservation-audit.test.ts +122 -0
- package/test/saeeol/compaction-skip-guard.test.ts +224 -0
- package/test/saeeol/compaction-smart-select.test.ts +100 -0
- package/test/saeeol/config/config.test.ts +166 -0
- package/test/saeeol/config/indexing-default-plugin.test.ts +82 -0
- package/test/saeeol/config/opentelemetry-default.test.ts +29 -0
- package/test/saeeol/config-gitignore.test.ts +70 -0
- package/test/saeeol/config-injector.test.ts +305 -0
- package/test/saeeol/config-resilience.test.ts +234 -0
- package/test/saeeol/config-validation.test.ts +183 -0
- package/test/saeeol/cost-propagation.test.ts +94 -0
- package/test/saeeol/cost-tracker-extended.test.ts +141 -0
- package/test/saeeol/cost-tracker.test.ts +64 -0
- package/test/saeeol/custom-provider-delete.test.ts +149 -0
- package/test/saeeol/diff-full.test.ts +226 -0
- package/test/saeeol/edit-permission-filediff.test.ts +223 -0
- package/test/saeeol/encoding.test.ts +364 -0
- package/test/saeeol/enhance-prompt.test.ts +61 -0
- package/test/saeeol/ensure-plan-dir.test.ts +32 -0
- package/test/saeeol/errors.test.ts +144 -0
- package/test/saeeol/external-directory-boundary.test.ts +96 -0
- package/test/saeeol/gateway-headers.test.ts +88 -0
- package/test/saeeol/help.test.ts +191 -0
- package/test/saeeol/ignore-migrator.test.ts +308 -0
- package/test/saeeol/indexing-auth.test.ts +45 -0
- package/test/saeeol/indexing-feature.test.ts +44 -0
- package/test/saeeol/indexing-label.test.ts +70 -0
- package/test/saeeol/indexing-startup.test.ts +381 -0
- package/test/saeeol/indexing-worktree.test.ts +73 -0
- package/test/saeeol/instruction.test.ts +136 -0
- package/test/saeeol/lancedb-runtime.test.ts +116 -0
- package/test/saeeol/loader-auth.test.ts +168 -0
- package/test/saeeol/local-model.test.ts +621 -0
- package/test/saeeol/logo.test.ts +31 -0
- package/test/saeeol/lsp-typescript-lightweight.test.ts +89 -0
- package/test/saeeol/mcp-branding.test.ts +33 -0
- package/test/saeeol/mcp-docker-rm.test.ts +32 -0
- package/test/saeeol/mcp-migrator.test.ts +736 -0
- package/test/saeeol/mcp-oauth-callback.test.ts +33 -0
- package/test/saeeol/memory-io.test.ts +198 -0
- package/test/saeeol/memory-paths.test.ts +87 -0
- package/test/saeeol/memory-security.test.ts +166 -0
- package/test/saeeol/model-cache-org.test.ts +164 -0
- package/test/saeeol/model-info-panel-utils.test.ts +52 -0
- package/test/saeeol/model-info-panel.types.test.ts +7 -0
- package/test/saeeol/models-401-fallback.test.ts +52 -0
- package/test/saeeol/modes-migrator.test.ts +320 -0
- package/test/saeeol/nvidia-headers.test.ts +74 -0
- package/test/saeeol/patch-jsonc.test.ts +73 -0
- package/test/saeeol/patch.test.ts +172 -0
- package/test/saeeol/paths.test.ts +265 -0
- package/test/saeeol/permission/config-paths.test.ts +174 -0
- package/test/saeeol/permission/env-read.test.ts +149 -0
- package/test/saeeol/permission/external-directory-allow.test.ts +327 -0
- package/test/saeeol/permission/next.always-rules.test.ts +882 -0
- package/test/saeeol/permission/next.reply-http.test.ts +205 -0
- package/test/saeeol/permission/next.reply-routing.test.ts +184 -0
- package/test/saeeol/plan-exit-detection.test.ts +494 -0
- package/test/saeeol/plan-followup.test.ts +1376 -0
- package/test/saeeol/project-config-update.test.ts +120 -0
- package/test/saeeol/project-id.test.ts +455 -0
- package/test/saeeol/provider-cost.test.ts +171 -0
- package/test/saeeol/provider-list-failed-state.test.ts +100 -0
- package/test/saeeol/question-dismiss-all.test.ts +174 -0
- package/test/saeeol/read-directory.test.ts +116 -0
- package/test/saeeol/rules-migrator.test.ts +257 -0
- package/test/saeeol/run-auto.test.ts +176 -0
- package/test/saeeol/run-network.test.ts +224 -0
- package/test/saeeol/semantic-search.test.ts +186 -0
- package/test/saeeol/server/permission-allow-everything.test.ts +125 -0
- package/test/saeeol/session/instruction-substitution.test.ts +72 -0
- package/test/saeeol/session/platform-attribution.test.ts +118 -0
- package/test/saeeol/session/session.test.ts +105 -0
- package/test/saeeol/session-compaction-cap.test.ts +399 -0
- package/test/saeeol/session-compaction-chunks.test.ts +501 -0
- package/test/saeeol/session-compaction-safety.test.ts +481 -0
- package/test/saeeol/session-fork-remap.test.ts +251 -0
- package/test/saeeol/session-import-service.test.ts +114 -0
- package/test/saeeol/session-list.test.ts +47 -0
- package/test/saeeol/session-message-metadata.test.ts +128 -0
- package/test/saeeol/session-overflow.test.ts +78 -0
- package/test/saeeol/session-processor-empty-tool-calls.test.ts +571 -0
- package/test/saeeol/session-processor-network-offline.test.ts +204 -0
- package/test/saeeol/session-processor-retry-limit.test.ts +238 -0
- package/test/saeeol/session-processor-review-telemetry.test.ts +82 -0
- package/test/saeeol/session-prompt-compaction-safety.test.ts +517 -0
- package/test/saeeol/session-prompt-queue.test.ts +815 -0
- package/test/saeeol/sessions/inflight-cache.test.ts +157 -0
- package/test/saeeol/sessions/ingest-queue.test.ts +402 -0
- package/test/saeeol/sessions/remote-protocol.test.ts +258 -0
- package/test/saeeol/sessions/remote-sender.test.ts +1036 -0
- package/test/saeeol/sessions/remote-ws.test.ts +367 -0
- package/test/saeeol/sessions/sessions-enable-remote.test.disable +181 -0
- package/test/saeeol/slot-prop-reactivity.test.ts +142 -0
- package/test/saeeol/snapshot-cache.test.ts +84 -0
- package/test/saeeol/snapshot-freeze-repro.test.ts +100 -0
- package/test/saeeol/snapshot-track-timeout.test.ts +519 -0
- package/test/saeeol/stats-subagent-cost.test.ts +123 -0
- package/test/saeeol/suggestion/auto-dismiss.test.ts +65 -0
- package/test/saeeol/suggestion/suggestion.test.ts +145 -0
- package/test/saeeol/suggestion/tool.test.ts +298 -0
- package/test/saeeol/summary-file-diff.test.ts +28 -0
- package/test/saeeol/system-prompt.test.ts +142 -0
- package/test/saeeol/task-nesting.test.ts +193 -0
- package/test/saeeol/telemetry/feedback.test.ts +8 -0
- package/test/saeeol/todo-view.test.ts +57 -0
- package/test/saeeol/tool-encoding.test.ts +455 -0
- package/test/saeeol/tool-registry-indexing-import-failure.test.ts +49 -0
- package/test/saeeol/tool-registry-indexing.test.ts +236 -0
- package/test/saeeol/tool-registry-semantic-import-failure.test.ts +55 -0
- package/test/saeeol/tool-task-model.test.ts +352 -0
- package/test/saeeol/transform-opus-4.7.test.ts +89 -0
- package/test/saeeol/tui-diff.test.ts +91 -0
- package/test/saeeol/tui-sync.test.ts +80 -0
- package/test/saeeol/util/url.test.ts +141 -0
- package/test/saeeol/workflows-migrator.test.ts +261 -0
- package/test/saeeol/worktree-diff-summary.test.ts +64 -0
- package/test/saeeol/worktree-diff.test.ts +223 -0
- package/test/saeeol/worktree-remove-lock.test.ts +82 -0
- package/test/server/AGENTS.md +15 -0
- package/test/server/contract.test.ts +357 -0
- package/test/server/experimental-session-list.test.ts +157 -0
- package/test/server/global-session-list.test.ts +155 -0
- package/test/server/httpapi-authorization.test.ts +103 -0
- package/test/server/httpapi-bridge.test.ts +440 -0
- package/test/server/httpapi-config.test.ts +67 -0
- package/test/server/httpapi-cors.test.ts +89 -0
- package/test/server/httpapi-event.test.ts +57 -0
- package/test/server/httpapi-experimental.test.ts +219 -0
- package/test/server/httpapi-file.test.ts +79 -0
- package/test/server/httpapi-instance-context.test.ts +237 -0
- package/test/server/httpapi-instance.legacy.test.ts +140 -0
- package/test/server/httpapi-instance.test.ts +83 -0
- package/test/server/httpapi-json-parity.test.ts +263 -0
- package/test/server/httpapi-mcp-oauth.test.ts +76 -0
- package/test/server/httpapi-mcp.test.ts +189 -0
- package/test/server/httpapi-provider.test.ts +153 -0
- package/test/server/httpapi-pty-websocket.test.ts +16 -0
- package/test/server/httpapi-pty.test.ts +175 -0
- package/test/server/httpapi-raw-route-auth.test.ts +89 -0
- package/test/server/httpapi-sdk.test.ts +681 -0
- package/test/server/httpapi-session.test.ts +464 -0
- package/test/server/httpapi-sync.test.ts +130 -0
- package/test/server/httpapi-tui.test.ts +121 -0
- package/test/server/httpapi-workspace-routing.test.ts +471 -0
- package/test/server/httpapi-workspace.test.ts +427 -0
- package/test/server/lib/conformance.ts +88 -0
- package/test/server/lib/stateful.ts +112 -0
- package/test/server/project-init-git.test.ts +113 -0
- package/test/server/proxy-util.test.ts +113 -0
- package/test/server/session-actions.test.ts +49 -0
- package/test/server/session-list.test.ts +238 -0
- package/test/server/session-messages.test.ts +167 -0
- package/test/server/session-select.test.ts +100 -0
- package/test/server/trace-attributes.test.ts +76 -0
- package/test/server/workspace-proxy.test.ts +165 -0
- package/test/server/workspace-routing.test.ts +85 -0
- package/test/session/compaction.test.ts +2420 -0
- package/test/session/instruction.test.ts +247 -0
- package/test/session/llm.test.ts +1273 -0
- package/test/session/message-v2.test.ts +1291 -0
- package/test/session/messages-pagination.test.ts +1173 -0
- package/test/session/network.test.ts +249 -0
- package/test/session/processor-effect.test.ts +847 -0
- package/test/session/prompt.test.ts +2131 -0
- package/test/session/retry.test.ts +340 -0
- package/test/session/revert-compact.test.ts +639 -0
- package/test/session/schema-decoding.test.ts +311 -0
- package/test/session/session-entry-stepper.test.ts +917 -0
- package/test/session/session-schema.test.ts +76 -0
- package/test/session/snapshot-tool-race.test.ts +257 -0
- package/test/session/structured-output-integration.test.ts +265 -0
- package/test/session/structured-output.test.ts +381 -0
- package/test/session/system.test.ts +73 -0
- package/test/share/share-next.test.ts +333 -0
- package/test/shell/shell.test.ts +99 -0
- package/test/skill/discovery.test.ts +116 -0
- package/test/skill/skill.test.ts +393 -0
- package/test/smoke/.tui-debug-output.txt +1 -0
- package/test/smoke/.tui-debug-plain.txt +1 -0
- package/test/smoke/.tui-walkthrough-report.txt +122 -0
- package/test/smoke/smoke-tui-pty.test.ts +123 -0
- package/test/smoke/smoke-tui.mjs +83 -0
- package/test/smoke/tui-walkthrough.test.ts +520 -0
- package/test/snapshot/snapshot.test.ts +1531 -0
- package/test/storage/db.test.ts +23 -0
- package/test/storage/json-migration.test.ts +832 -0
- package/test/storage/storage.test.ts +293 -0
- package/test/suggestion/suggestion.test.ts +1 -0
- package/test/sync/index.test.ts +256 -0
- package/test/tool/__snapshots__/parameters.test.ts.snap +500 -0
- package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
- package/test/tool/apply_patch.test.ts +614 -0
- package/test/tool/bash.test.ts +1225 -0
- package/test/tool/diagnostics-filter.test.ts +55 -0
- package/test/tool/edit.test.ts +754 -0
- package/test/tool/external-directory.test.ts +169 -0
- package/test/tool/fixtures/large-image.png +0 -0
- package/test/tool/fixtures/models-api.json +65179 -0
- package/test/tool/glob.test.ts +107 -0
- package/test/tool/grep.test.ts +114 -0
- package/test/tool/lsp.test.ts +187 -0
- package/test/tool/parameters.test.ts +243 -0
- package/test/tool/question.test.ts +129 -0
- package/test/tool/read.test.ts +500 -0
- package/test/tool/recall.test.ts +151 -0
- package/test/tool/registry.test.ts +203 -0
- package/test/tool/skill.test.ts +135 -0
- package/test/tool/suggest.test.ts +1 -0
- package/test/tool/task.test.ts +612 -0
- package/test/tool/tool-define.test.ts +99 -0
- package/test/tool/truncation.test.ts +260 -0
- package/test/tool/webfetch.test.ts +103 -0
- package/test/tool/write.test.ts +291 -0
- package/test/util/data-url.test.ts +14 -0
- package/test/util/effect-zod.test.ts +754 -0
- package/test/util/error.test.ts +38 -0
- package/test/util/filesystem.test.ts +656 -0
- package/test/util/format.test.ts +59 -0
- package/test/util/glob.test.ts +164 -0
- package/test/util/iife.test.ts +36 -0
- package/test/util/lazy.test.ts +50 -0
- package/test/util/lock.test.ts +72 -0
- package/test/util/log.test.ts +86 -0
- package/test/util/module.test.ts +59 -0
- package/test/util/process.test.ts +128 -0
- package/test/util/timeout.test.ts +21 -0
- package/test/util/which.test.ts +100 -0
- package/test/util/wildcard.test.ts +90 -0
- package/test/workspace/workspace-restore.test.ts +296 -0
- package/src/provider/models-snapshot.d.ts +0 -2
- package/src/provider/models-snapshot.js +0 -3
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { test, expect } from "bun:test"
|
|
2
|
+
import { Permission } from "../../src/permission"
|
|
3
|
+
|
|
4
|
+
// toConfig tests (inverse of fromConfig)
|
|
5
|
+
|
|
6
|
+
test("toConfig - single wildcard rule uses object format", () => {
|
|
7
|
+
const result = Permission.toConfig([{ permission: "read", pattern: "*", action: "allow" }])
|
|
8
|
+
expect(result).toEqual({ read: { "*": "allow" } })
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test("toConfig - single non-wildcard rule uses object format", () => {
|
|
12
|
+
const result = Permission.toConfig([{ permission: "bash", pattern: "npm *", action: "allow" }])
|
|
13
|
+
expect(result).toEqual({ bash: { "npm *": "allow" } })
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test("toConfig - multiple rules for same permission use object format", () => {
|
|
17
|
+
const result = Permission.toConfig([
|
|
18
|
+
{ permission: "bash", pattern: "*", action: "ask" },
|
|
19
|
+
{ permission: "bash", pattern: "npm *", action: "allow" },
|
|
20
|
+
])
|
|
21
|
+
expect(result).toEqual({ bash: { "*": "ask", "npm *": "allow" } })
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test("toConfig - mixed permissions", () => {
|
|
25
|
+
const result = Permission.toConfig([
|
|
26
|
+
{ permission: "read", pattern: "*", action: "allow" },
|
|
27
|
+
{ permission: "bash", pattern: "npm *", action: "allow" },
|
|
28
|
+
{ permission: "bash", pattern: "git *", action: "allow" },
|
|
29
|
+
])
|
|
30
|
+
expect(result).toEqual({
|
|
31
|
+
read: { "*": "allow" },
|
|
32
|
+
bash: { "npm *": "allow", "git *": "allow" },
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test("toConfig - empty rules returns empty object", () => {
|
|
37
|
+
const result = Permission.toConfig([])
|
|
38
|
+
expect(result).toEqual({})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test("toConfig - wildcard then specific promotes to object", () => {
|
|
42
|
+
const result = Permission.toConfig([
|
|
43
|
+
{ permission: "bash", pattern: "*", action: "ask" },
|
|
44
|
+
{ permission: "bash", pattern: "rm *", action: "deny" },
|
|
45
|
+
])
|
|
46
|
+
expect(result).toEqual({ bash: { "*": "ask", "rm *": "deny" } })
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test("toConfig - roundtrip with fromConfig (simple) always uses object format", () => {
|
|
50
|
+
const config = { read: "allow" as const, bash: "ask" as const }
|
|
51
|
+
const rules = Permission.fromConfig(config)
|
|
52
|
+
const result = Permission.toConfig(rules)
|
|
53
|
+
// toConfig always uses object format to avoid erasing existing granular rules on merge
|
|
54
|
+
expect(result).toEqual({ read: { "*": "allow" }, bash: { "*": "ask" } })
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test("toConfig - roundtrip with fromConfig (object)", () => {
|
|
58
|
+
const config = { bash: { "*": "ask" as const, "npm *": "allow" as const, "git *": "allow" as const } }
|
|
59
|
+
const rules = Permission.fromConfig(config)
|
|
60
|
+
const result = Permission.toConfig(rules)
|
|
61
|
+
expect(result).toEqual(config)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test("toConfig - scalar-only permission uses scalar format", () => {
|
|
65
|
+
const result = Permission.toConfig([{ permission: "websearch", pattern: "*", action: "allow" }])
|
|
66
|
+
expect(result).toEqual({ websearch: "allow" })
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test("toConfig - scalar-only permission with non-wildcard pattern is skipped", () => {
|
|
70
|
+
// doom_loop uses always: [toolName], so pattern can be "bash" etc.
|
|
71
|
+
// Non-wildcard patterns for scalar-only permissions can't be represented
|
|
72
|
+
// in the config schema — they only work in-memory (known limitation).
|
|
73
|
+
const result = Permission.toConfig([{ permission: "doom_loop", pattern: "bash", action: "allow" }])
|
|
74
|
+
expect(result).toEqual({})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test("toConfig - mixed scalar-only and rule-capable permissions", () => {
|
|
78
|
+
const result = Permission.toConfig([
|
|
79
|
+
{ permission: "websearch", pattern: "*", action: "allow" },
|
|
80
|
+
{ permission: "todowrite", pattern: "*", action: "allow" },
|
|
81
|
+
{ permission: "bash", pattern: "npm *", action: "allow" },
|
|
82
|
+
])
|
|
83
|
+
expect(result).toEqual({
|
|
84
|
+
websearch: "allow",
|
|
85
|
+
todowrite: "allow",
|
|
86
|
+
bash: { "npm *": "allow" },
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Tests for null delete sentinel handling (null = "remove this key from config")
|
|
91
|
+
|
|
92
|
+
test("fromConfig - null entries in PermissionObject are skipped", () => {
|
|
93
|
+
const config = { bash: { "*": "ask" as const, "npm *": null } }
|
|
94
|
+
const rules = Permission.fromConfig(config)
|
|
95
|
+
// null is a delete sentinel — only the non-null entry should produce a rule
|
|
96
|
+
expect(rules).toEqual([{ permission: "bash", pattern: "*", action: "ask" }])
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test("fromConfig - null top-level PermissionRule is skipped", () => {
|
|
100
|
+
const config = { bash: null }
|
|
101
|
+
const rules = Permission.fromConfig(config)
|
|
102
|
+
expect(rules).toEqual([])
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test("toConfig - null existing entry is treated as absent (new rule wins)", () => {
|
|
106
|
+
// If result[permission] is null (shouldn't happen in practice but defensive),
|
|
107
|
+
// the new rule should be written as a fresh object entry.
|
|
108
|
+
const result = Permission.toConfig([{ permission: "bash", pattern: "npm *", action: "allow" }])
|
|
109
|
+
expect(result).toEqual({ bash: { "npm *": "allow" } })
|
|
110
|
+
})
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { afterEach, describe, test, expect } from "bun:test"
|
|
2
|
+
import { Permission } from "../src/permission"
|
|
3
|
+
import { Config } from "@/config/config"
|
|
4
|
+
import { Instance } from "../src/project/instance"
|
|
5
|
+
import { disposeAllInstances, tmpdir } from "./fixture/fixture"
|
|
6
|
+
import { AppRuntime } from "../src/effect/app-runtime"
|
|
7
|
+
|
|
8
|
+
const load = () => AppRuntime.runPromise(Config.Service.use((svc) => svc.get()))
|
|
9
|
+
|
|
10
|
+
afterEach(async () => {
|
|
11
|
+
await disposeAllInstances()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe("Permission.evaluate for permission.task", () => {
|
|
15
|
+
const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): Permission.Ruleset =>
|
|
16
|
+
Object.entries(rules).map(([pattern, action]) => ({
|
|
17
|
+
permission: "task",
|
|
18
|
+
pattern,
|
|
19
|
+
action,
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
test("returns ask when no match (default)", () => {
|
|
23
|
+
expect(Permission.evaluate("task", "code-reviewer", []).action).toBe("ask")
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test("returns deny for explicit deny", () => {
|
|
27
|
+
const ruleset = createRuleset({ "code-reviewer": "deny" })
|
|
28
|
+
expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("deny")
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test("returns allow for explicit allow", () => {
|
|
32
|
+
const ruleset = createRuleset({ "code-reviewer": "allow" })
|
|
33
|
+
expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("allow")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test("returns ask for explicit ask", () => {
|
|
37
|
+
const ruleset = createRuleset({ "code-reviewer": "ask" })
|
|
38
|
+
expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("ask")
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test("matches wildcard patterns with deny", () => {
|
|
42
|
+
const ruleset = createRuleset({ "orchestrator-*": "deny" })
|
|
43
|
+
expect(Permission.evaluate("task", "orchestrator-fast", ruleset).action).toBe("deny")
|
|
44
|
+
expect(Permission.evaluate("task", "orchestrator-slow", ruleset).action).toBe("deny")
|
|
45
|
+
expect(Permission.evaluate("task", "general", ruleset).action).toBe("ask")
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test("matches wildcard patterns with allow", () => {
|
|
49
|
+
const ruleset = createRuleset({ "orchestrator-*": "allow" })
|
|
50
|
+
expect(Permission.evaluate("task", "orchestrator-fast", ruleset).action).toBe("allow")
|
|
51
|
+
expect(Permission.evaluate("task", "orchestrator-slow", ruleset).action).toBe("allow")
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test("matches wildcard patterns with ask", () => {
|
|
55
|
+
const ruleset = createRuleset({ "orchestrator-*": "ask" })
|
|
56
|
+
expect(Permission.evaluate("task", "orchestrator-fast", ruleset).action).toBe("ask")
|
|
57
|
+
const globalRuleset = createRuleset({ "*": "ask" })
|
|
58
|
+
expect(Permission.evaluate("task", "code-reviewer", globalRuleset).action).toBe("ask")
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test("later rules take precedence (last match wins)", () => {
|
|
62
|
+
const ruleset = createRuleset({
|
|
63
|
+
"orchestrator-*": "deny",
|
|
64
|
+
"orchestrator-fast": "allow",
|
|
65
|
+
})
|
|
66
|
+
expect(Permission.evaluate("task", "orchestrator-fast", ruleset).action).toBe("allow")
|
|
67
|
+
expect(Permission.evaluate("task", "orchestrator-slow", ruleset).action).toBe("deny")
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test("matches global wildcard", () => {
|
|
71
|
+
expect(Permission.evaluate("task", "any-agent", createRuleset({ "*": "allow" })).action).toBe("allow")
|
|
72
|
+
expect(Permission.evaluate("task", "any-agent", createRuleset({ "*": "deny" })).action).toBe("deny")
|
|
73
|
+
expect(Permission.evaluate("task", "any-agent", createRuleset({ "*": "ask" })).action).toBe("ask")
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe("Permission.disabled for task tool", () => {
|
|
78
|
+
// Note: The `disabled` function checks if a TOOL should be completely removed from the tool list.
|
|
79
|
+
// It only disables a tool when there's a rule with `pattern: "*"` and `action: "deny"`.
|
|
80
|
+
// It does NOT evaluate complex subagent patterns - those are handled at runtime by `evaluate`.
|
|
81
|
+
const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): Permission.Ruleset =>
|
|
82
|
+
Object.entries(rules).map(([pattern, action]) => ({
|
|
83
|
+
permission: "task",
|
|
84
|
+
pattern,
|
|
85
|
+
action,
|
|
86
|
+
}))
|
|
87
|
+
|
|
88
|
+
test("task tool is disabled when global deny pattern exists (even with specific allows)", () => {
|
|
89
|
+
// When "*": "deny" exists, the task tool is disabled because the disabled() function
|
|
90
|
+
// only checks for wildcard deny patterns - it doesn't consider that specific subagents might be allowed
|
|
91
|
+
const ruleset = createRuleset({
|
|
92
|
+
"orchestrator-*": "allow",
|
|
93
|
+
"*": "deny",
|
|
94
|
+
})
|
|
95
|
+
const disabled = Permission.disabled(["task", "bash", "read"], ruleset)
|
|
96
|
+
// The task tool IS disabled because there's a pattern: "*" with action: "deny"
|
|
97
|
+
expect(disabled.has("task")).toBe(true)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test("task tool is disabled when global deny pattern exists (even with ask overrides)", () => {
|
|
101
|
+
const ruleset = createRuleset({
|
|
102
|
+
"orchestrator-*": "ask",
|
|
103
|
+
"*": "deny",
|
|
104
|
+
})
|
|
105
|
+
const disabled = Permission.disabled(["task"], ruleset)
|
|
106
|
+
// The task tool IS disabled because there's a pattern: "*" with action: "deny"
|
|
107
|
+
expect(disabled.has("task")).toBe(true)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test("task tool is disabled when global deny pattern exists", () => {
|
|
111
|
+
const ruleset = createRuleset({ "*": "deny" })
|
|
112
|
+
const disabled = Permission.disabled(["task"], ruleset)
|
|
113
|
+
expect(disabled.has("task")).toBe(true)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test("task tool is NOT disabled when only specific patterns are denied (no wildcard)", () => {
|
|
117
|
+
// The disabled() function only disables tools when pattern: "*" && action: "deny"
|
|
118
|
+
// Specific subagent denies don't disable the task tool - those are handled at runtime
|
|
119
|
+
const ruleset = createRuleset({
|
|
120
|
+
"orchestrator-*": "deny",
|
|
121
|
+
general: "deny",
|
|
122
|
+
})
|
|
123
|
+
const disabled = Permission.disabled(["task"], ruleset)
|
|
124
|
+
// The task tool is NOT disabled because no rule has pattern: "*" with action: "deny"
|
|
125
|
+
expect(disabled.has("task")).toBe(false)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test("task tool is enabled when no task rules exist (default ask)", () => {
|
|
129
|
+
const disabled = Permission.disabled(["task"], [])
|
|
130
|
+
expect(disabled.has("task")).toBe(false)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test("task tool is NOT disabled when last wildcard pattern is allow", () => {
|
|
134
|
+
// Last matching rule wins - if wildcard allow comes after wildcard deny, tool is enabled
|
|
135
|
+
const ruleset = createRuleset({
|
|
136
|
+
"*": "deny",
|
|
137
|
+
"orchestrator-coder": "allow",
|
|
138
|
+
})
|
|
139
|
+
const disabled = Permission.disabled(["task"], ruleset)
|
|
140
|
+
// The disabled() function uses findLast and checks if the last matching rule
|
|
141
|
+
// has pattern: "*" and action: "deny". In this case, the last rule matching
|
|
142
|
+
// "task" permission has pattern "orchestrator-coder", not "*", so not disabled
|
|
143
|
+
expect(disabled.has("task")).toBe(false)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// Integration tests that load permissions from real config files
|
|
148
|
+
describe("permission.task with real config files", () => {
|
|
149
|
+
test("loads task permissions from saeeol.json config", async () => {
|
|
150
|
+
await using tmp = await tmpdir({
|
|
151
|
+
git: true,
|
|
152
|
+
config: {
|
|
153
|
+
permission: {
|
|
154
|
+
task: {
|
|
155
|
+
"*": "allow",
|
|
156
|
+
"code-reviewer": "deny",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
await Instance.provide({
|
|
162
|
+
directory: tmp.path,
|
|
163
|
+
fn: async () => {
|
|
164
|
+
const config = await load()
|
|
165
|
+
const ruleset = Permission.fromConfig(config.permission ?? {})
|
|
166
|
+
// general and orchestrator-fast should be allowed, code-reviewer denied
|
|
167
|
+
expect(Permission.evaluate("task", "general", ruleset).action).toBe("allow")
|
|
168
|
+
expect(Permission.evaluate("task", "orchestrator-fast", ruleset).action).toBe("allow")
|
|
169
|
+
expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("deny")
|
|
170
|
+
},
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test("loads task permissions with wildcard patterns from config", async () => {
|
|
175
|
+
await using tmp = await tmpdir({
|
|
176
|
+
git: true,
|
|
177
|
+
config: {
|
|
178
|
+
permission: {
|
|
179
|
+
task: {
|
|
180
|
+
"*": "ask",
|
|
181
|
+
"orchestrator-*": "deny",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
await Instance.provide({
|
|
187
|
+
directory: tmp.path,
|
|
188
|
+
fn: async () => {
|
|
189
|
+
const config = await load()
|
|
190
|
+
const ruleset = Permission.fromConfig(config.permission ?? {})
|
|
191
|
+
// general and code-reviewer should be ask, orchestrator-* denied
|
|
192
|
+
expect(Permission.evaluate("task", "general", ruleset).action).toBe("ask")
|
|
193
|
+
expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("ask")
|
|
194
|
+
expect(Permission.evaluate("task", "orchestrator-fast", ruleset).action).toBe("deny")
|
|
195
|
+
},
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test("evaluate respects task permission from config", async () => {
|
|
200
|
+
await using tmp = await tmpdir({
|
|
201
|
+
git: true,
|
|
202
|
+
config: {
|
|
203
|
+
permission: {
|
|
204
|
+
task: {
|
|
205
|
+
general: "allow",
|
|
206
|
+
"code-reviewer": "deny",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
})
|
|
211
|
+
await Instance.provide({
|
|
212
|
+
directory: tmp.path,
|
|
213
|
+
fn: async () => {
|
|
214
|
+
const config = await load()
|
|
215
|
+
const ruleset = Permission.fromConfig(config.permission ?? {})
|
|
216
|
+
expect(Permission.evaluate("task", "general", ruleset).action).toBe("allow")
|
|
217
|
+
expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("deny")
|
|
218
|
+
// Unspecified agents default to "ask"
|
|
219
|
+
expect(Permission.evaluate("task", "unknown-agent", ruleset).action).toBe("ask")
|
|
220
|
+
},
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
test("mixed permission config with task and other tools", async () => {
|
|
225
|
+
await using tmp = await tmpdir({
|
|
226
|
+
git: true,
|
|
227
|
+
config: {
|
|
228
|
+
permission: {
|
|
229
|
+
bash: "allow",
|
|
230
|
+
edit: "ask",
|
|
231
|
+
task: {
|
|
232
|
+
"*": "deny",
|
|
233
|
+
general: "allow",
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
})
|
|
238
|
+
await Instance.provide({
|
|
239
|
+
directory: tmp.path,
|
|
240
|
+
fn: async () => {
|
|
241
|
+
const config = await load()
|
|
242
|
+
const ruleset = Permission.fromConfig(config.permission ?? {})
|
|
243
|
+
|
|
244
|
+
// Verify task permissions
|
|
245
|
+
expect(Permission.evaluate("task", "general", ruleset).action).toBe("allow")
|
|
246
|
+
expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("deny")
|
|
247
|
+
|
|
248
|
+
// Verify other tool permissions
|
|
249
|
+
expect(Permission.evaluate("bash", "*", ruleset).action).toBe("allow")
|
|
250
|
+
expect(Permission.evaluate("edit", "*", ruleset).action).toBe("ask")
|
|
251
|
+
|
|
252
|
+
// Verify disabled tools
|
|
253
|
+
const disabled = Permission.disabled(["bash", "edit", "task"], ruleset)
|
|
254
|
+
expect(disabled.has("bash")).toBe(false)
|
|
255
|
+
expect(disabled.has("edit")).toBe(false)
|
|
256
|
+
// task is NOT disabled because disabled() uses findLast, and the last rule
|
|
257
|
+
// matching "task" permission is {pattern: "general", action: "allow"}, not pattern: "*"
|
|
258
|
+
expect(disabled.has("task")).toBe(false)
|
|
259
|
+
},
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test("task tool disabled when global deny comes last in config", async () => {
|
|
264
|
+
await using tmp = await tmpdir({
|
|
265
|
+
git: true,
|
|
266
|
+
config: {
|
|
267
|
+
permission: {
|
|
268
|
+
task: {
|
|
269
|
+
general: "allow",
|
|
270
|
+
"code-reviewer": "allow",
|
|
271
|
+
"*": "deny",
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
})
|
|
276
|
+
await Instance.provide({
|
|
277
|
+
directory: tmp.path,
|
|
278
|
+
fn: async () => {
|
|
279
|
+
const config = await load()
|
|
280
|
+
const ruleset = Permission.fromConfig(config.permission ?? {})
|
|
281
|
+
|
|
282
|
+
// Last matching rule wins - "*" deny is last, so all agents are denied
|
|
283
|
+
expect(Permission.evaluate("task", "general", ruleset).action).toBe("deny")
|
|
284
|
+
expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("deny")
|
|
285
|
+
expect(Permission.evaluate("task", "unknown", ruleset).action).toBe("deny")
|
|
286
|
+
|
|
287
|
+
// Since "*": "deny" is the last rule, disabled() finds it with findLast
|
|
288
|
+
// and sees pattern: "*" with action: "deny", so task is disabled
|
|
289
|
+
const disabled = Permission.disabled(["task"], ruleset)
|
|
290
|
+
expect(disabled.has("task")).toBe(true)
|
|
291
|
+
},
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test("task tool NOT disabled when specific allow comes last in config", async () => {
|
|
296
|
+
await using tmp = await tmpdir({
|
|
297
|
+
git: true,
|
|
298
|
+
config: {
|
|
299
|
+
permission: {
|
|
300
|
+
task: {
|
|
301
|
+
"*": "deny",
|
|
302
|
+
general: "allow",
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
})
|
|
307
|
+
await Instance.provide({
|
|
308
|
+
directory: tmp.path,
|
|
309
|
+
fn: async () => {
|
|
310
|
+
const config = await load()
|
|
311
|
+
const ruleset = Permission.fromConfig(config.permission ?? {})
|
|
312
|
+
|
|
313
|
+
// Evaluate uses findLast - "general" allow comes after "*" deny
|
|
314
|
+
expect(Permission.evaluate("task", "general", ruleset).action).toBe("allow")
|
|
315
|
+
// Other agents still denied by the earlier "*" deny
|
|
316
|
+
expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("deny")
|
|
317
|
+
|
|
318
|
+
// disabled() uses findLast and checks if the last rule has pattern: "*" with action: "deny"
|
|
319
|
+
// In this case, the last rule is {pattern: "general", action: "allow"}, not pattern: "*"
|
|
320
|
+
// So the task tool is NOT disabled (even though most subagents are denied)
|
|
321
|
+
const disabled = Permission.disabled(["task"], ruleset)
|
|
322
|
+
expect(disabled.has("task")).toBe(false)
|
|
323
|
+
},
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import fs from "fs/promises"
|
|
4
|
+
import { Effect } from "effect"
|
|
5
|
+
import { tmpdir } from "../fixture/fixture"
|
|
6
|
+
import { Instance } from "../../src/project/instance"
|
|
7
|
+
import { ProviderAuth } from "@/provider/auth"
|
|
8
|
+
import { ProviderID } from "../../src/provider/schema"
|
|
9
|
+
|
|
10
|
+
describe("plugin.auth-override", () => {
|
|
11
|
+
test("user plugin overrides built-in github-copilot auth", async () => {
|
|
12
|
+
await using tmp = await tmpdir({
|
|
13
|
+
init: async (dir) => {
|
|
14
|
+
const pluginDir = path.join(dir, ".saeeol", "plugin")
|
|
15
|
+
await fs.mkdir(pluginDir, { recursive: true })
|
|
16
|
+
|
|
17
|
+
await Bun.write(
|
|
18
|
+
path.join(pluginDir, "custom-copilot-auth.ts"),
|
|
19
|
+
[
|
|
20
|
+
"export default {",
|
|
21
|
+
' id: "demo.custom-copilot-auth",',
|
|
22
|
+
" server: async () => ({",
|
|
23
|
+
" auth: {",
|
|
24
|
+
' provider: "github-copilot",',
|
|
25
|
+
" methods: [",
|
|
26
|
+
' { type: "api", label: "Test Override Auth" },',
|
|
27
|
+
" ],",
|
|
28
|
+
" loader: async () => ({ access: 'test-token' }),",
|
|
29
|
+
" },",
|
|
30
|
+
" }),",
|
|
31
|
+
"}",
|
|
32
|
+
"",
|
|
33
|
+
].join("\n"),
|
|
34
|
+
)
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
await using plain = await tmpdir()
|
|
39
|
+
|
|
40
|
+
const methods = await Instance.provide({
|
|
41
|
+
directory: tmp.path,
|
|
42
|
+
fn: async () => {
|
|
43
|
+
return Effect.runPromise(
|
|
44
|
+
ProviderAuth.Service.use((svc) => svc.methods()).pipe(Effect.provide(ProviderAuth.defaultLayer)),
|
|
45
|
+
)
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const plainMethods = await Instance.provide({
|
|
50
|
+
directory: plain.path,
|
|
51
|
+
fn: async () => {
|
|
52
|
+
return Effect.runPromise(
|
|
53
|
+
ProviderAuth.Service.use((svc) => svc.methods()).pipe(Effect.provide(ProviderAuth.defaultLayer)),
|
|
54
|
+
)
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const copilot = methods[ProviderID.make("github-copilot")]
|
|
59
|
+
expect(copilot).toBeDefined()
|
|
60
|
+
expect(copilot.length).toBe(1)
|
|
61
|
+
expect(copilot[0].label).toBe("Test Override Auth")
|
|
62
|
+
expect(plainMethods[ProviderID.make("github-copilot")][0].label).not.toBe("Test Override Auth")
|
|
63
|
+
}, 30000) // Increased timeout for plugin installation
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const file = path.join(import.meta.dir, "../../src/plugin/index.ts")
|
|
67
|
+
|
|
68
|
+
describe("plugin.config-hook-error-isolation", () => {
|
|
69
|
+
test("config hooks are individually error-isolated in the layer factory", async () => {
|
|
70
|
+
const src = await Bun.file(file).text()
|
|
71
|
+
|
|
72
|
+
// Each hook's config call is wrapped in Effect.tryPromise with error logging + Effect.ignore
|
|
73
|
+
expect(src).toContain("plugin config hook failed")
|
|
74
|
+
|
|
75
|
+
const pattern =
|
|
76
|
+
/for\s*\(const hook of hooks\)\s*\{[\s\S]*?Effect\.tryPromise[\s\S]*?\.config\?\.\([\s\S]*?plugin config hook failed[\s\S]*?Effect\.ignore/
|
|
77
|
+
expect(pattern.test(src)).toBe(true)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { CloudflareAIGatewayAuthPlugin } from "@/plugin/cloudflare"
|
|
3
|
+
|
|
4
|
+
const pluginInput = {
|
|
5
|
+
client: {} as never,
|
|
6
|
+
project: {} as never,
|
|
7
|
+
directory: "",
|
|
8
|
+
worktree: "",
|
|
9
|
+
experimental_workspace: {
|
|
10
|
+
register() {},
|
|
11
|
+
},
|
|
12
|
+
serverUrl: new URL("https://example.com"),
|
|
13
|
+
$: {} as never,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function makeHookInput(overrides: { providerID?: string; apiId?: string; reasoning?: boolean }) {
|
|
17
|
+
return {
|
|
18
|
+
sessionID: "s",
|
|
19
|
+
agent: "a",
|
|
20
|
+
provider: {} as never,
|
|
21
|
+
message: {} as never,
|
|
22
|
+
model: {
|
|
23
|
+
providerID: overrides.providerID ?? "cloudflare-ai-gateway",
|
|
24
|
+
api: { id: overrides.apiId ?? "openai/gpt-5.2-codex", url: "", npm: "ai-gateway-provider" },
|
|
25
|
+
capabilities: {
|
|
26
|
+
reasoning: overrides.reasoning ?? true,
|
|
27
|
+
temperature: false,
|
|
28
|
+
attachment: true,
|
|
29
|
+
toolcall: true,
|
|
30
|
+
input: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
31
|
+
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
32
|
+
interleaved: false,
|
|
33
|
+
},
|
|
34
|
+
} as never,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeHookOutput() {
|
|
39
|
+
return { temperature: 0, topP: 1, topK: 0, maxOutputTokens: 32_000 as number | undefined, options: {} }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test("omits maxOutputTokens for openai reasoning models on cloudflare-ai-gateway", async () => {
|
|
43
|
+
const hooks = await CloudflareAIGatewayAuthPlugin(pluginInput)
|
|
44
|
+
const out = makeHookOutput()
|
|
45
|
+
await hooks["chat.params"]!(makeHookInput({ apiId: "openai/gpt-5.2-codex", reasoning: true }), out)
|
|
46
|
+
expect(out.maxOutputTokens).toBeUndefined()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test("keeps maxOutputTokens for openai non-reasoning models", async () => {
|
|
50
|
+
const hooks = await CloudflareAIGatewayAuthPlugin(pluginInput)
|
|
51
|
+
const out = makeHookOutput()
|
|
52
|
+
await hooks["chat.params"]!(makeHookInput({ apiId: "openai/gpt-4-turbo", reasoning: false }), out)
|
|
53
|
+
expect(out.maxOutputTokens).toBe(32_000)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("keeps maxOutputTokens for non-openai reasoning models on cloudflare-ai-gateway", async () => {
|
|
57
|
+
const hooks = await CloudflareAIGatewayAuthPlugin(pluginInput)
|
|
58
|
+
const out = makeHookOutput()
|
|
59
|
+
await hooks["chat.params"]!(makeHookInput({ apiId: "anthropic/claude-sonnet-4-5", reasoning: true }), out)
|
|
60
|
+
expect(out.maxOutputTokens).toBe(32_000)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test("ignores non-cloudflare-ai-gateway providers", async () => {
|
|
64
|
+
const hooks = await CloudflareAIGatewayAuthPlugin(pluginInput)
|
|
65
|
+
const out = makeHookOutput()
|
|
66
|
+
await hooks["chat.params"]!(makeHookInput({ providerID: "openai", apiId: "gpt-5.2-codex", reasoning: true }), out)
|
|
67
|
+
expect(out.maxOutputTokens).toBe(32_000)
|
|
68
|
+
})
|