@phuetz/code-buddy 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.fr.md +56 -0
- package/README.md +73 -16
- package/codebuddy-runtime.json +5 -5
- package/dist/agent/codebuddy-agent.d.ts +18 -0
- package/dist/agent/codebuddy-agent.js +45 -8
- package/dist/agent/execution/agent-executor.d.ts +0 -9
- package/dist/agent/execution/agent-executor.js +165 -12
- package/dist/agent/execution/context-pipeline.d.ts +9 -0
- package/dist/agent/execution/context-pipeline.js +64 -2
- package/dist/agent/execution/tool-loop-guard.d.ts +76 -0
- package/dist/agent/execution/tool-loop-guard.js +165 -0
- package/dist/agent/execution/tool-selection-strategy.js +19 -2
- package/dist/agent/facades/session-facade.d.ts +0 -6
- package/dist/agent/facades/session-facade.js +0 -45
- package/dist/agent/hermes-claw-migrate.d.ts +0 -21
- package/dist/agent/hermes-claw-migrate.js +9 -4
- package/dist/agent/hermes-protocol-gateways.js +8 -1
- package/dist/agent/repo-profiler.js +17 -4
- package/dist/agent/self-improvement/authored-tool-runtime.js +10 -2
- package/dist/agent/self-improvement/evolution/ast-novelty.d.ts +5 -0
- package/dist/agent/self-improvement/evolution/ast-novelty.js +58 -23
- package/dist/agent/self-improvement/evolution/code-variant-store.d.ts +3 -0
- package/dist/agent/self-improvement/evolution/evolution-engine.js +2 -0
- package/dist/agent/self-improvement/evolution/protected-paths.js +6 -0
- package/dist/agent/self-improvement/evolution/variant-fitness.d.ts +13 -2
- package/dist/agent/self-improvement/evolution/variant-fitness.js +184 -58
- package/dist/agent/self-improvement/evolution/worktree-scorer.d.ts +2 -0
- package/dist/agent/self-improvement/evolution/worktree-scorer.js +15 -0
- package/dist/agent/self-improvement/evolutionary-archive.d.ts +2 -1
- package/dist/agent/self-improvement/evolutionary-archive.js +34 -5
- package/dist/agent/self-improvement/proposal-store.d.ts +1 -0
- package/dist/agent/self-improvement/proposal-store.js +79 -17
- package/dist/agent/self-improvement/skill-apply-journal.d.ts +44 -0
- package/dist/agent/self-improvement/skill-apply-journal.js +178 -0
- package/dist/agent/self-improvement/skill-behavior-benchmark.d.ts +3 -0
- package/dist/agent/self-improvement/skill-behavior-benchmark.js +19 -0
- package/dist/agent/self-improvement/skill-behavior-gate.d.ts +34 -0
- package/dist/agent/self-improvement/skill-behavior-gate.js +96 -0
- package/dist/agent/self-improvement/skill-engine.d.ts +22 -0
- package/dist/agent/self-improvement/skill-engine.js +385 -13
- package/dist/agent/self-improvement/skill-gate.d.ts +2 -0
- package/dist/agent/self-improvement/skill-gate.js +17 -2
- package/dist/agent/self-improvement/skill-mutator.d.ts +6 -1
- package/dist/agent/self-improvement/skill-mutator.js +51 -15
- package/dist/agent/self-improvement/skill-types.d.ts +2 -1
- package/dist/agent/self-improvement/types.d.ts +11 -0
- package/dist/agent/tool-handler.d.ts +1 -1
- package/dist/agent/tool-handler.js +28 -9
- package/dist/auth/profile-manager.js +1 -1
- package/dist/channels/companion-channel-turn.d.ts +53 -4
- package/dist/channels/companion-channel-turn.js +218 -16
- package/dist/channels/slash-parity.d.ts +4 -6
- package/dist/channels/slash-parity.js +9 -44
- package/dist/cli/session-commands.d.ts +9 -0
- package/dist/cli/session-commands.js +45 -5
- package/dist/cli/session-picker.d.ts +43 -0
- package/dist/cli/session-picker.js +128 -0
- package/dist/codebuddy/a2a-call-tool-defs.d.ts +26 -0
- package/dist/codebuddy/a2a-call-tool-defs.js +12 -0
- package/dist/codebuddy/fleet-tool-defs.d.ts +1 -0
- package/dist/codebuddy/fleet-tool-defs.js +24 -0
- package/dist/codebuddy/providers/chatgpt-headers.d.ts +18 -0
- package/dist/codebuddy/providers/chatgpt-headers.js +37 -0
- package/dist/codebuddy/providers/provider-chatgpt-responses.d.ts +2 -1
- package/dist/codebuddy/providers/provider-chatgpt-responses.js +10 -23
- package/dist/codebuddy/providers/provider-openai-compat.js +1 -1
- package/dist/codebuddy/ragchat-tool-defs.d.ts +31 -0
- package/dist/codebuddy/ragchat-tool-defs.js +19 -0
- package/dist/codebuddy/resource-catalog-tool-defs.d.ts +28 -0
- package/dist/codebuddy/resource-catalog-tool-defs.js +18 -0
- package/dist/codebuddy/tool-definitions/agent-tools.js +2 -2
- package/dist/codebuddy/tool-definitions/code-exec-tools.js +5 -1
- package/dist/codebuddy/tool-definitions/cron-tools.js +6 -0
- package/dist/codebuddy/tool-definitions/self-describe-tools.js +6 -1
- package/dist/codebuddy/tools.d.ts +0 -6
- package/dist/codebuddy/tools.js +25 -3
- package/dist/commands/cli/daemon-commands.js +3 -1
- package/dist/commands/cli/evolve-command.js +21 -5
- package/dist/commands/cli/fleet-collaboration-commands.d.ts +2 -0
- package/dist/commands/cli/fleet-collaboration-commands.js +57 -0
- package/dist/commands/cli/fleet-commands.js +29 -0
- package/dist/commands/cli/fleet-mission-commands.d.ts +3 -0
- package/dist/commands/cli/fleet-mission-commands.js +72 -0
- package/dist/commands/cli/fleet-rooms-commands.d.ts +10 -0
- package/dist/commands/cli/fleet-rooms-commands.js +289 -0
- package/dist/commands/cli/tools-commands.js +2 -1
- package/dist/commands/cli/triage-command.d.ts +8 -0
- package/dist/commands/cli/triage-command.js +72 -0
- package/dist/commands/cli/utility-commands.js +25 -1
- package/dist/commands/client-dispatcher.d.ts +7 -0
- package/dist/commands/client-dispatcher.js +73 -21
- package/dist/commands/cron-cli/index.d.ts +3 -1
- package/dist/commands/cron-cli/index.js +25 -0
- package/dist/commands/device-auth.d.ts +10 -0
- package/dist/commands/device-auth.js +91 -0
- package/dist/commands/enhanced-command-handler.d.ts +9 -0
- package/dist/commands/enhanced-command-handler.js +28 -6
- package/dist/commands/handlers/agent-handlers.d.ts +1 -1
- package/dist/commands/handlers/agent-handlers.js +4 -4
- package/dist/commands/handlers/channel-handlers.js +53 -2
- package/dist/commands/handlers/export-handlers.d.ts +3 -2
- package/dist/commands/handlers/export-handlers.js +55 -28
- package/dist/commands/handlers/extra-handlers.js +5 -1
- package/dist/commands/handlers/grill-me-handler.js +26 -18
- package/dist/commands/handlers/memory-handlers.d.ts +6 -2
- package/dist/commands/handlers/memory-handlers.js +35 -15
- package/dist/commands/handlers/missing-handlers.d.ts +2 -2
- package/dist/commands/handlers/missing-handlers.js +4 -4
- package/dist/commands/handlers/resources-handler.d.ts +12 -0
- package/dist/commands/handlers/resources-handler.js +52 -0
- package/dist/commands/handlers/starter-handlers.d.ts +1 -1
- package/dist/commands/handlers/starter-handlers.js +32 -2
- package/dist/commands/handlers/switch-handler.d.ts +1 -1
- package/dist/commands/handlers/switch-handler.js +15 -0
- package/dist/commands/handlers/test-handlers.js +4 -86
- package/dist/commands/handlers/ui-handlers.js +11 -5
- package/dist/commands/login-chatgpt.d.ts +8 -0
- package/dist/commands/login-chatgpt.js +17 -0
- package/dist/commands/mcp-import.d.ts +11 -0
- package/dist/commands/mcp-import.js +67 -0
- package/dist/commands/mcp-invoke.d.ts +2 -0
- package/dist/commands/mcp-invoke.js +68 -0
- package/dist/commands/mcp.js +4 -0
- package/dist/commands/pipeline.d.ts +7 -1
- package/dist/commands/pipeline.js +104 -7
- package/dist/commands/resources.d.ts +2 -0
- package/dist/commands/resources.js +64 -0
- package/dist/commands/skills-cli/index.js +35 -12
- package/dist/commands/slash/builtin-commands.js +16 -3
- package/dist/commands/slash/index.d.ts +3 -1
- package/dist/commands/slash/index.js +2 -0
- package/dist/commands/slash/surfaces.d.ts +53 -0
- package/dist/commands/slash/surfaces.js +177 -0
- package/dist/commands/slash/types.d.ts +17 -0
- package/dist/commands/try.d.ts +1 -1
- package/dist/commands/try.js +1 -1
- package/dist/companion/companion-identity.d.ts +43 -0
- package/dist/companion/companion-identity.js +136 -0
- package/dist/companion/companion-toolset.d.ts +66 -0
- package/dist/companion/companion-toolset.js +368 -0
- package/dist/companion/companion-turn.d.ts +10 -2
- package/dist/companion/companion-turn.js +38 -1
- package/dist/config/code-exec-policy.d.ts +25 -0
- package/dist/config/code-exec-policy.js +39 -0
- package/dist/config/model-tools.d.ts +9 -0
- package/dist/config/model-tools.js +4 -0
- package/dist/context/context-manager-v2.js +2 -2
- package/dist/context/lm-resizer-compressor.js +3 -2
- package/dist/context/lm-resizer-diagnostics.d.ts +9 -0
- package/dist/context/lm-resizer-diagnostics.js +30 -0
- package/dist/context/tool-output-masking.js +3 -4
- package/dist/curator/curator.d.ts +6 -0
- package/dist/curator/curator.js +53 -3
- package/dist/daemon/cron-agent-bridge.d.ts +6 -0
- package/dist/daemon/cron-agent-bridge.js +31 -4
- package/dist/doctor/index.d.ts +11 -2
- package/dist/doctor/index.js +84 -21
- package/dist/doctor/integrations.d.ts +179 -0
- package/dist/doctor/integrations.js +227 -0
- package/dist/doctor/triage.d.ts +108 -0
- package/dist/doctor/triage.js +304 -0
- package/dist/fleet/collaboration.d.ts +44 -0
- package/dist/fleet/collaboration.js +170 -0
- package/dist/fleet/fleet-listener.d.ts +1 -0
- package/dist/fleet/fleet-listener.js +87 -46
- package/dist/fleet/fleet-registry.d.ts +2 -0
- package/dist/fleet/resource-catalog.d.ts +245 -0
- package/dist/fleet/resource-catalog.js +211 -0
- package/dist/fleet/rooms/room-access.d.ts +138 -0
- package/dist/fleet/rooms/room-access.js +196 -0
- package/dist/fleet/rooms/room-client.d.ts +158 -0
- package/dist/fleet/rooms/room-client.js +685 -0
- package/dist/fleet/rooms/room-event.d.ts +139 -0
- package/dist/fleet/rooms/room-event.js +330 -0
- package/dist/fleet/rooms/room-filter.d.ts +48 -0
- package/dist/fleet/rooms/room-filter.js +152 -0
- package/dist/fleet/rooms/room-hub.d.ts +154 -0
- package/dist/fleet/rooms/room-hub.js +363 -0
- package/dist/fleet/rooms/room-identity.d.ts +26 -0
- package/dist/fleet/rooms/room-identity.js +82 -0
- package/dist/fleet/rooms/room-lock.d.ts +34 -0
- package/dist/fleet/rooms/room-lock.js +180 -0
- package/dist/fleet/rooms/room-observations.d.ts +7 -0
- package/dist/fleet/rooms/room-observations.js +61 -0
- package/dist/fleet/rooms/room-server.d.ts +28 -0
- package/dist/fleet/rooms/room-server.js +75 -0
- package/dist/fleet/rooms/room-store.d.ts +119 -0
- package/dist/fleet/rooms/room-store.js +448 -0
- package/dist/fleet/rooms/room-ws-bridge.d.ts +27 -0
- package/dist/fleet/rooms/room-ws-bridge.js +137 -0
- package/dist/fleet/sensory-mission-bridge.d.ts +42 -0
- package/dist/fleet/sensory-mission-bridge.js +136 -0
- package/dist/git/worktree-sessions.js +1 -0
- package/dist/harness/contract.d.ts +4 -4
- package/dist/harness/fleet-supervisor.d.ts +16 -0
- package/dist/harness/fleet-supervisor.js +64 -0
- package/dist/harness/index.d.ts +4 -0
- package/dist/harness/index.js +4 -0
- package/dist/harness/mission-runner.d.ts +3 -0
- package/dist/harness/mission-runner.js +53 -0
- package/dist/harness/mission-store.d.ts +93 -0
- package/dist/harness/mission-store.js +297 -0
- package/dist/harness/tool-harness.d.ts +56 -0
- package/dist/harness/tool-harness.js +153 -0
- package/dist/hooks/use-enhanced-input.js +48 -4
- package/dist/hooks/use-input-handler.d.ts +1 -0
- package/dist/hooks/use-input-handler.js +82 -22
- package/dist/hooks/use-input-history.js +1 -0
- package/dist/identity/lisa-introspection.js +5 -2
- package/dist/identity/operational-self-model.d.ts +2 -0
- package/dist/identity/operational-self-model.js +4 -0
- package/dist/index.js +52 -6
- package/dist/mcp/client.js +6 -0
- package/dist/mcp/config.d.ts +20 -0
- package/dist/mcp/config.js +121 -8
- package/dist/mcp/import-normalize.d.ts +12 -0
- package/dist/mcp/import-normalize.js +114 -0
- package/dist/mcp/transports.d.ts +10 -5
- package/dist/mcp/transports.js +61 -99
- package/dist/mcp/types.d.ts +5 -0
- package/dist/media/comfyui-recipe-contract.d.ts +7 -7
- package/dist/memory/facts-memory.d.ts +11 -1
- package/dist/memory/facts-memory.js +47 -8
- package/dist/memory/memory-candidate-queue.d.ts +2 -2
- package/dist/memory/memory-candidate-queue.js +6 -6
- package/dist/memory/persistent-memory.d.ts +13 -8
- package/dist/memory/persistent-memory.js +77 -30
- package/dist/nodes/device-node.js +5 -9
- package/dist/observability/mobile-supervision-gateway-contract.js +20 -7
- package/dist/observability/run-event-writer.d.ts +21 -0
- package/dist/observability/run-event-writer.js +81 -0
- package/dist/observability/run-store.d.ts +5 -0
- package/dist/observability/run-store.js +18 -13
- package/dist/observability/run-trajectory.js +1 -1
- package/dist/optimization/prompt-cache.d.ts +19 -2
- package/dist/optimization/prompt-cache.js +41 -6
- package/dist/orchestration/orchestrator.d.ts +21 -0
- package/dist/orchestration/orchestrator.js +122 -15
- package/dist/persistence/session-content.d.ts +8 -0
- package/dist/persistence/session-content.js +76 -0
- package/dist/persistence/session-handoff.d.ts +64 -0
- package/dist/persistence/session-handoff.js +80 -0
- package/dist/persistence/session-history.d.ts +4 -0
- package/dist/persistence/session-history.js +38 -0
- package/dist/persistence/session-store.d.ts +9 -0
- package/dist/persistence/session-store.js +48 -20
- package/dist/personas/persona-manager.js +17 -1
- package/dist/plugins/code-explorer/CodeExplorerManager.d.ts +10 -4
- package/dist/plugins/code-explorer/CodeExplorerManager.js +69 -16
- package/dist/plugins/plugin-manager.js +10 -1
- package/dist/prompts/prompt-manager.js +11 -4
- package/dist/prompts/system-base.js +6 -2
- package/dist/protocols/a2a/codebuddy-executor.d.ts +11 -2
- package/dist/protocols/a2a/codebuddy-executor.js +9 -8
- package/dist/protocols/a2a/index.d.ts +1 -1
- package/dist/protocols/a2a/jsonrpc-v1.d.ts +41 -0
- package/dist/protocols/a2a/jsonrpc-v1.js +145 -0
- package/dist/protocols/a2a/peer-config.d.ts +20 -0
- package/dist/protocols/a2a/peer-config.js +55 -0
- package/dist/providers/codex-oauth.js +30 -6
- package/dist/providers/model-provider-compat.js +2 -1
- package/dist/sandbox/os-sandbox.d.ts +1 -1
- package/dist/scheduler/cron-scheduler.d.ts +15 -2
- package/dist/scheduler/cron-scheduler.js +35 -4
- package/dist/scheduler/job-notepad.d.ts +69 -0
- package/dist/scheduler/job-notepad.js +339 -0
- package/dist/security/audit-logger.d.ts +1 -1
- package/dist/security/compute-confinement.d.ts +4 -0
- package/dist/security/compute-confinement.js +89 -0
- package/dist/security/session-encryption.d.ts +4 -0
- package/dist/security/session-encryption.js +31 -0
- package/dist/sensory/voice-loop.d.ts +17 -0
- package/dist/sensory/voice-loop.js +72 -15
- package/dist/server/auth/device-session-context.d.ts +9 -0
- package/dist/server/auth/device-session-context.js +13 -0
- package/dist/server/auth/device-store.d.ts +89 -0
- package/dist/server/auth/device-store.js +261 -0
- package/dist/server/auth/device-token.d.ts +3 -0
- package/dist/server/auth/device-token.js +6 -0
- package/dist/server/auth/jwt.js +4 -0
- package/dist/server/index.d.ts +0 -10
- package/dist/server/index.js +28 -0
- package/dist/server/middleware/auth.js +3 -2
- package/dist/server/routes/a2a-jsonrpc.d.ts +9 -0
- package/dist/server/routes/a2a-jsonrpc.js +66 -0
- package/dist/server/routes/a2a-protocol.d.ts +12 -0
- package/dist/server/routes/a2a-protocol.js +1 -1
- package/dist/server/routes/device-auth.d.ts +4 -0
- package/dist/server/routes/device-auth.js +40 -0
- package/dist/server/types.d.ts +4 -0
- package/dist/server/websocket/handler.d.ts +13 -0
- package/dist/server/websocket/handler.js +102 -17
- package/dist/services/prompt-builder.d.ts +2 -0
- package/dist/services/prompt-builder.js +13 -5
- package/dist/services/runtime-settings-context.d.ts +73 -0
- package/dist/services/runtime-settings-context.js +69 -0
- package/dist/skills/executor.js +2 -0
- package/dist/skills/hub.js +16 -14
- package/dist/skills/local-inventory.d.ts +21 -0
- package/dist/skills/local-inventory.js +72 -0
- package/dist/skills/skill-loader.js +2 -2
- package/dist/skills/skill-usage-store.d.ts +49 -0
- package/dist/skills/skill-usage-store.js +121 -0
- package/dist/testing/ai-integration-tests.js +8 -5
- package/dist/themes/theme-manager.d.ts +12 -0
- package/dist/themes/theme-manager.js +29 -0
- package/dist/themes/theme-schema.d.ts +24 -24
- package/dist/tools/a2a-call-tool.d.ts +34 -0
- package/dist/tools/a2a-call-tool.js +75 -0
- package/dist/tools/bash/streaming-executor.d.ts +1 -0
- package/dist/tools/bash/streaming-executor.js +78 -90
- package/dist/tools/code-exec-preflight-runner.d.ts +41 -0
- package/dist/tools/code-exec-preflight-runner.js +254 -0
- package/dist/tools/code-exec-preflight.d.ts +44 -0
- package/dist/tools/code-exec-preflight.js +501 -0
- package/dist/tools/code-exec-tool.d.ts +48 -1
- package/dist/tools/code-exec-tool.js +191 -47
- package/dist/tools/comment-watcher.js +29 -12
- package/dist/tools/core-code-inspection.d.ts +56 -0
- package/dist/tools/core-code-inspection.js +86 -0
- package/dist/tools/create-skill-tool.d.ts +2 -3
- package/dist/tools/create-skill-tool.js +16 -75
- package/dist/tools/cronjob-tool.d.ts +1 -0
- package/dist/tools/cronjob-tool.js +1 -0
- package/dist/tools/env-doctor-tool.d.ts +2 -0
- package/dist/tools/env-doctor-tool.js +4 -3
- package/dist/tools/execute-code-rpc-invoker.d.ts +1 -1
- package/dist/tools/execute-code-rpc-invoker.js +35 -19
- package/dist/tools/execute-code-runner.d.ts +2 -0
- package/dist/tools/execute-code-runner.js +52 -17
- package/dist/tools/fleet-room-tool.d.ts +104 -0
- package/dist/tools/fleet-room-tool.js +124 -0
- package/dist/tools/integration-tool-hints.d.ts +4 -0
- package/dist/tools/integration-tool-hints.js +19 -0
- package/dist/tools/list-peers-tool.d.ts +3 -0
- package/dist/tools/list-peers-tool.js +11 -4
- package/dist/tools/media-generation-tool.d.ts +16 -1
- package/dist/tools/media-generation-tool.js +341 -9
- package/dist/tools/merge-conflict-tool.js +53 -25
- package/dist/tools/metadata.d.ts +1 -5
- package/dist/tools/metadata.js +21 -28
- package/dist/tools/ragchat-tool.d.ts +39 -0
- package/dist/tools/ragchat-tool.js +105 -0
- package/dist/tools/registry/fleet-tools.d.ts +9 -0
- package/dist/tools/registry/fleet-tools.js +22 -1
- package/dist/tools/registry/knowledge-tools.d.ts +2 -2
- package/dist/tools/registry/knowledge-tools.js +2 -2
- package/dist/tools/registry/lessons-tools.d.ts +7 -0
- package/dist/tools/registry/lessons-tools.js +43 -3
- package/dist/tools/registry/memory-tools.d.ts +1 -1
- package/dist/tools/registry/memory-tools.js +8 -8
- package/dist/tools/registry/research-tools.d.ts +1 -0
- package/dist/tools/registry/research-tools.js +4 -13
- package/dist/tools/registry/self-describe-tools.d.ts +1 -1
- package/dist/tools/registry/self-describe-tools.js +43 -30
- package/dist/tools/resource-catalog-tool.d.ts +38 -0
- package/dist/tools/resource-catalog-tool.js +46 -0
- package/dist/tools/skills-inspection-tool.js +30 -1
- package/dist/tools/stream-tool-output.d.ts +3 -0
- package/dist/tools/stream-tool-output.js +33 -0
- package/dist/tools/tool-call-scheduler.d.ts +10 -0
- package/dist/tools/tool-call-scheduler.js +58 -0
- package/dist/tools/tool-effect.d.ts +5 -0
- package/dist/tools/tool-effect.js +26 -0
- package/dist/tools/tool-search.d.ts +12 -11
- package/dist/tools/tool-search.js +67 -26
- package/dist/tools/types.d.ts +6 -0
- package/dist/ui/components/ChatHistory.d.ts +1 -1
- package/dist/ui/components/ChatHistory.js +2 -2
- package/dist/ui/components/ChatInput.d.ts +2 -1
- package/dist/ui/components/ChatInput.js +42 -88
- package/dist/ui/components/ChatInterface.d.ts +4 -2
- package/dist/ui/components/ChatInterface.js +27 -63
- package/dist/ui/components/CommandSuggestions.d.ts +0 -1
- package/dist/ui/components/CommandSuggestions.js +4 -6
- package/dist/ui/components/FileAutocomplete.js +1 -1
- package/dist/ui/components/KeyboardHelp.js +8 -6
- package/dist/ui/components/ModelSelection.js +6 -5
- package/dist/ui/components/StatusBar.d.ts +2 -1
- package/dist/ui/components/StatusBar.js +8 -7
- package/dist/ui/context/theme-context.js +3 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/ui/utils/markdown-renderer.js +4 -13
- package/dist/utils/atomic-write.d.ts +22 -0
- package/dist/utils/atomic-write.js +29 -0
- package/dist/utils/bounded-output.d.ts +16 -0
- package/dist/utils/bounded-output.js +63 -0
- package/dist/utils/device-store-file.d.ts +4 -0
- package/dist/utils/device-store-file.js +40 -0
- package/dist/utils/export-manager.d.ts +9 -0
- package/dist/utils/export-manager.js +19 -0
- package/dist/utils/first-use-hints.d.ts +25 -0
- package/dist/utils/first-use-hints.js +58 -0
- package/dist/utils/history-manager.js +1 -1
- package/dist/utils/update-notifier.js +16 -18
- package/dist/wizard/environment-detection.d.ts +5 -1
- package/dist/wizard/environment-detection.js +49 -7
- package/dist/wizard/onboarding.d.ts +5 -1
- package/dist/wizard/onboarding.js +90 -30
- package/dist/wizard/provider-onboarding.d.ts +2 -0
- package/dist/wizard/provider-onboarding.js +55 -17
- package/package.json +7 -3
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/** Explicit resources; health is not proof that a model/capability works. */
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { writeJsonAtomic } from '../utils/atomic-write.js';
|
|
8
|
+
const identifier = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,95}$/);
|
|
9
|
+
export const resourceSchema = z.object({
|
|
10
|
+
id: identifier,
|
|
11
|
+
kind: z.enum(['inference', 'comfyui', 'storage', 'rag', 'database', 'docker', 'code-explorer', 'camera', 'microphone']),
|
|
12
|
+
hostId: identifier,
|
|
13
|
+
declaredCapabilities: z.array(identifier).min(1).max(32),
|
|
14
|
+
endpointRef: z.string().regex(/^[A-Z][A-Z0-9_]{0,95}$/),
|
|
15
|
+
healthPath: z.enum(['/health', '/healthz', '/api/health', '/v1/models', '/api/tags', '/system_stats', '/_ping']),
|
|
16
|
+
permissions: z.object({ probe: z.boolean(), use: z.boolean() }).strict(),
|
|
17
|
+
ttlMs: z.number().int().min(1000).max(3600000).default(60000),
|
|
18
|
+
timeoutMs: z.number().int().min(50).max(5000).default(1000),
|
|
19
|
+
}).strict();
|
|
20
|
+
const observationSchema = z.object({
|
|
21
|
+
state: z.enum(['online', 'offline', 'unknown']),
|
|
22
|
+
checkedAt: z.number().nonnegative(),
|
|
23
|
+
lastSeen: z.number().nonnegative().nullable(),
|
|
24
|
+
latencyMs: z.number().nonnegative().nullable(),
|
|
25
|
+
endpointFingerprint: z.string(),
|
|
26
|
+
reason: z.string(),
|
|
27
|
+
}).strict();
|
|
28
|
+
const entrySchema = z.object({ resource: resourceSchema, observation: observationSchema.nullable() }).strict();
|
|
29
|
+
const storeSchema = z.object({ version: z.literal(1), entries: z.array(entrySchema).max(100) }).strict();
|
|
30
|
+
/** Declarations and observations only: never the resolved endpoint value or the internal fingerprint. */
|
|
31
|
+
export function publicResourceStatus(entry) {
|
|
32
|
+
return { resource: entry.resource, state: entry.state, reason: entry.reason,
|
|
33
|
+
checkedAt: entry.observation?.checkedAt ?? null, lastSeen: entry.observation?.lastSeen ?? null,
|
|
34
|
+
latencyMs: entry.observation?.latencyMs ?? null, load: entry.load, usageConfirmed: entry.usageConfirmed };
|
|
35
|
+
}
|
|
36
|
+
/** The RagChat connector reads this reference only; a selection never redirects it. */
|
|
37
|
+
export const RAGCHAT_ENDPOINT_REF = 'RAGCHAT_BASE_URL';
|
|
38
|
+
export function selectionWarning(selected) {
|
|
39
|
+
if (!selected || selected.resource.kind !== 'rag' || selected.resource.endpointRef === RAGCHAT_ENDPOINT_REF)
|
|
40
|
+
return undefined;
|
|
41
|
+
return `ragchat_search interroge toujours ${RAGCHAT_ENDPOINT_REF} ; cette ressource référence ${selected.resource.endpointRef} et la sélection ne redirige pas le connecteur.`;
|
|
42
|
+
}
|
|
43
|
+
const allowedPaths = {
|
|
44
|
+
inference: ['/health', '/healthz', '/v1/models', '/api/tags'],
|
|
45
|
+
comfyui: ['/system_stats'],
|
|
46
|
+
rag: ['/health', '/healthz', '/api/health'],
|
|
47
|
+
docker: ['/_ping'],
|
|
48
|
+
'code-explorer': ['/health', '/healthz', '/api/health'],
|
|
49
|
+
};
|
|
50
|
+
function endpoint(resource) {
|
|
51
|
+
const raw = process.env[resource.endpointRef];
|
|
52
|
+
if (!raw)
|
|
53
|
+
throw new Error('ENDPOINT_UNCONFIGURED');
|
|
54
|
+
let url;
|
|
55
|
+
try {
|
|
56
|
+
url = new URL(raw);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new Error('ENDPOINT_INVALID');
|
|
60
|
+
}
|
|
61
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== '/') {
|
|
62
|
+
throw new Error('ENDPOINT_INVALID');
|
|
63
|
+
}
|
|
64
|
+
url.pathname = resource.healthPath;
|
|
65
|
+
return { url, fingerprint: createHash('sha256').update(url.href).digest('hex') };
|
|
66
|
+
}
|
|
67
|
+
export class ResourceCatalog {
|
|
68
|
+
filename;
|
|
69
|
+
now;
|
|
70
|
+
constructor(filename = path.join(os.homedir(), '.codebuddy', 'resources', 'catalog.json'), now = Date.now) {
|
|
71
|
+
this.filename = filename;
|
|
72
|
+
this.now = now;
|
|
73
|
+
}
|
|
74
|
+
async read() {
|
|
75
|
+
try {
|
|
76
|
+
const stat = await fs.lstat(this.filename);
|
|
77
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024)
|
|
78
|
+
throw new Error('INVALID_CATALOG_FILE');
|
|
79
|
+
const parsed = storeSchema.safeParse(JSON.parse(await fs.readFile(this.filename, 'utf8')));
|
|
80
|
+
if (!parsed.success || new Set(parsed.data.entries.map(e => e.resource.id)).size !== parsed.data.entries.length) {
|
|
81
|
+
throw new Error('INVALID_CATALOG');
|
|
82
|
+
}
|
|
83
|
+
return parsed.data;
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error.code === 'ENOENT')
|
|
87
|
+
return { version: 1, entries: [] };
|
|
88
|
+
throw new Error('INVALID_CATALOG: existing data was not replaced');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async update(action) {
|
|
92
|
+
const directory = path.dirname(this.filename);
|
|
93
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
94
|
+
const stat = await fs.lstat(directory);
|
|
95
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
96
|
+
throw new Error('INVALID_CATALOG_DIRECTORY');
|
|
97
|
+
await fs.chmod(directory, 0o700);
|
|
98
|
+
const lock = `${this.filename}.lock`;
|
|
99
|
+
try {
|
|
100
|
+
await fs.mkdir(lock, { mode: 0o700 });
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
throw new Error('CATALOG_BUSY: another writer or unrecovered lock');
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const store = await this.read();
|
|
107
|
+
const result = await action(store);
|
|
108
|
+
await writeJsonAtomic(this.filename, store, { mode: 0o600 });
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
await fs.rmdir(lock);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async add(input) {
|
|
116
|
+
const parsed = resourceSchema.safeParse(input);
|
|
117
|
+
if (!parsed.success)
|
|
118
|
+
throw new Error('INVALID_RESOURCE: check schema; inline endpoints and extra fields are forbidden');
|
|
119
|
+
return this.update(async (store) => {
|
|
120
|
+
if (store.entries.some(e => e.resource.id === parsed.data.id))
|
|
121
|
+
throw new Error('RESOURCE_EXISTS');
|
|
122
|
+
if (store.entries.length >= 100)
|
|
123
|
+
throw new Error('CATALOG_FULL');
|
|
124
|
+
store.entries.push({ resource: parsed.data, observation: null });
|
|
125
|
+
return parsed.data;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async remove(id) {
|
|
129
|
+
return this.update(async (store) => {
|
|
130
|
+
const index = store.entries.findIndex(e => e.resource.id === id);
|
|
131
|
+
if (index < 0)
|
|
132
|
+
throw new Error('RESOURCE_NOT_FOUND');
|
|
133
|
+
store.entries.splice(index, 1);
|
|
134
|
+
return { removed: id };
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
status(entry) {
|
|
138
|
+
const { resource, observation } = entry;
|
|
139
|
+
let state = observation?.state ?? 'unknown';
|
|
140
|
+
let reason = observation?.reason ?? 'NEVER_PROBED';
|
|
141
|
+
if (observation && (this.now() - observation.checkedAt >= resource.ttlMs || observation.checkedAt > this.now())) {
|
|
142
|
+
state = 'stale';
|
|
143
|
+
reason = 'OBSERVATION_EXPIRED';
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
if (observation && observation.endpointFingerprint !== endpoint(resource).fingerprint) {
|
|
147
|
+
state = 'unknown';
|
|
148
|
+
reason = 'ENDPOINT_CHANGED';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
state = 'unknown';
|
|
153
|
+
reason = 'ENDPOINT_UNCONFIGURED_OR_INVALID';
|
|
154
|
+
}
|
|
155
|
+
return { ...entry, state, reason, load: null, usageConfirmed: false };
|
|
156
|
+
}
|
|
157
|
+
async list() { return (await this.read()).entries.map(e => this.status(e)); }
|
|
158
|
+
async probe(id) {
|
|
159
|
+
// Lock covers the bounded probe so a later probe cannot be overwritten by an older completion.
|
|
160
|
+
return this.update(async (store) => {
|
|
161
|
+
const entry = store.entries.find(e => e.resource.id === id);
|
|
162
|
+
if (!entry)
|
|
163
|
+
throw new Error('RESOURCE_NOT_FOUND');
|
|
164
|
+
const r = entry.resource;
|
|
165
|
+
if (!r.permissions.probe)
|
|
166
|
+
throw new Error('PROBE_NOT_PERMITTED');
|
|
167
|
+
if (!allowedPaths[r.kind]?.includes(r.healthPath))
|
|
168
|
+
throw new Error('NO_READ_ONLY_PROBE_FOR_KIND_PATH');
|
|
169
|
+
const target = endpoint(r);
|
|
170
|
+
const start = this.now();
|
|
171
|
+
let state = 'offline';
|
|
172
|
+
let reason = 'PROBE_FAILED';
|
|
173
|
+
try {
|
|
174
|
+
const response = await fetch(target.url, { method: 'GET', redirect: 'manual', signal: AbortSignal.timeout(r.timeoutMs) });
|
|
175
|
+
state = response.ok ? 'online' : 'offline';
|
|
176
|
+
reason = response.ok ? 'HTTP_HEALTH_OK_NOT_USAGE_PROOF' : `HTTP_${response.status}`;
|
|
177
|
+
// Never parse service content, model lists, database contents or device frames.
|
|
178
|
+
await response.body?.cancel();
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
state = 'offline';
|
|
182
|
+
reason = 'UNREACHABLE_OR_TIMEOUT';
|
|
183
|
+
}
|
|
184
|
+
const checkedAt = this.now();
|
|
185
|
+
entry.observation = { state, reason, checkedAt, lastSeen: state === 'online' ? checkedAt
|
|
186
|
+
: entry.observation?.endpointFingerprint === target.fingerprint ? entry.observation.lastSeen : null,
|
|
187
|
+
latencyMs: state === 'online' ? Math.max(0, checkedAt - start) : null, endpointFingerprint: target.fingerprint };
|
|
188
|
+
return this.status(entry);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
async select(capability, kind) {
|
|
192
|
+
if (!identifier.safeParse(capability).success)
|
|
193
|
+
throw new Error('INVALID_CAPABILITY');
|
|
194
|
+
const excluded = [];
|
|
195
|
+
const candidates = (await this.list()).filter(e => {
|
|
196
|
+
const reason = kind && e.resource.kind !== kind ? 'KIND_MISMATCH'
|
|
197
|
+
: !e.resource.declaredCapabilities.includes(capability) ? 'CAPABILITY_NOT_DECLARED'
|
|
198
|
+
: !e.resource.permissions.use ? 'USE_NOT_PERMITTED'
|
|
199
|
+
: e.state !== 'online' ? e.reason : null;
|
|
200
|
+
if (reason)
|
|
201
|
+
excluded.push({ id: e.resource.id, reason });
|
|
202
|
+
return !reason;
|
|
203
|
+
});
|
|
204
|
+
// Load unknown: do not fabricate free capacity. Stable deterministic latency/id choice only.
|
|
205
|
+
candidates.sort((a, b) => (a.observation?.latencyMs ?? Infinity) - (b.observation?.latencyMs ?? Infinity)
|
|
206
|
+
|| a.resource.id.localeCompare(b.resource.id));
|
|
207
|
+
return { selected: candidates[0] ?? null, excluded,
|
|
208
|
+
reason: candidates.length ? 'FRESH_HEALTH_DECLARED_CAPABILITY_LOAD_UNKNOWN' : 'NO_ELIGIBLE_RESOURCE' };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=resource-catalog.js.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fleet rooms — membership and access policy.
|
|
3
|
+
*
|
|
4
|
+
* Buzz makes channel membership "the only gate", enforced by the relay before
|
|
5
|
+
* any read or write (`ARCHITECTURE.md` §7, `crates/buzz-relay/src/handlers/req.rs`).
|
|
6
|
+
* Here the hub operator declares members (public keys) and rooms in one file:
|
|
7
|
+
*
|
|
8
|
+
* ```json
|
|
9
|
+
* {
|
|
10
|
+
* "version": 1,
|
|
11
|
+
* "members": { "<pubkey hex>": { "name": "peer-alpha", "principals": ["key:abc"] } },
|
|
12
|
+
* "rooms": { "general": { "members": ["<pubkey hex>"], "readers": ["<pubkey hex>"] } }
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* - `rooms.<id>.members` may read and write; `readers` may only read.
|
|
17
|
+
* - `principals` binds a key to WebSocket identities: when the field is ABSENT
|
|
18
|
+
* the key is accepted on any authenticated connection; when it is a list the
|
|
19
|
+
* connection principal must be in it; an EMPTY list refuses every connection.
|
|
20
|
+
*
|
|
21
|
+
* Fail-closed: a missing, unreadable or invalid file yields no member and no
|
|
22
|
+
* room. Refresh window: the file's `mtimeMs`/`ctimeMs`/size/inode are checked at
|
|
23
|
+
* most once per `reloadIntervalMs` (default 1000 ms), so a revocation applies
|
|
24
|
+
* to the first check after that window — including deliveries to subscriptions
|
|
25
|
+
* that are already open.
|
|
26
|
+
*
|
|
27
|
+
* @module fleet/rooms/room-access
|
|
28
|
+
*/
|
|
29
|
+
import { z } from 'zod';
|
|
30
|
+
export declare const MAX_ROOM_ACCESS_MEMBERS = 1024;
|
|
31
|
+
export declare const MAX_ROOM_ACCESS_ROOMS = 1024;
|
|
32
|
+
declare const configSchema: z.ZodObject<{
|
|
33
|
+
version: z.ZodLiteral<1>;
|
|
34
|
+
members: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
35
|
+
name: z.ZodString;
|
|
36
|
+
principals: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
37
|
+
}, "strict", z.ZodTypeAny, {
|
|
38
|
+
name: string;
|
|
39
|
+
principals?: string[] | undefined;
|
|
40
|
+
}, {
|
|
41
|
+
name: string;
|
|
42
|
+
principals?: string[] | undefined;
|
|
43
|
+
}>>;
|
|
44
|
+
rooms: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
45
|
+
description: z.ZodOptional<z.ZodString>;
|
|
46
|
+
members: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
47
|
+
readers: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
48
|
+
}, "strict", z.ZodTypeAny, {
|
|
49
|
+
members: string[];
|
|
50
|
+
readers: string[];
|
|
51
|
+
description?: string | undefined;
|
|
52
|
+
}, {
|
|
53
|
+
description?: string | undefined;
|
|
54
|
+
members?: string[] | undefined;
|
|
55
|
+
readers?: string[] | undefined;
|
|
56
|
+
}>>;
|
|
57
|
+
}, "strict", z.ZodTypeAny, {
|
|
58
|
+
version: 1;
|
|
59
|
+
members: Record<string, {
|
|
60
|
+
name: string;
|
|
61
|
+
principals?: string[] | undefined;
|
|
62
|
+
}>;
|
|
63
|
+
rooms: Record<string, {
|
|
64
|
+
members: string[];
|
|
65
|
+
readers: string[];
|
|
66
|
+
description?: string | undefined;
|
|
67
|
+
}>;
|
|
68
|
+
}, {
|
|
69
|
+
version: 1;
|
|
70
|
+
members: Record<string, {
|
|
71
|
+
name: string;
|
|
72
|
+
principals?: string[] | undefined;
|
|
73
|
+
}>;
|
|
74
|
+
rooms: Record<string, {
|
|
75
|
+
description?: string | undefined;
|
|
76
|
+
members?: string[] | undefined;
|
|
77
|
+
readers?: string[] | undefined;
|
|
78
|
+
}>;
|
|
79
|
+
}>;
|
|
80
|
+
export type RoomAccessConfigInput = z.input<typeof configSchema>;
|
|
81
|
+
export interface RoomMember {
|
|
82
|
+
readonly pubkey: string;
|
|
83
|
+
readonly name: string;
|
|
84
|
+
/** `undefined`: no binding. `[]`: refuse all connections. */
|
|
85
|
+
readonly principals?: readonly string[];
|
|
86
|
+
}
|
|
87
|
+
export type RoomAccessLevel = 'write' | 'read';
|
|
88
|
+
interface CompiledRoom {
|
|
89
|
+
description?: string;
|
|
90
|
+
writers: ReadonlySet<string>;
|
|
91
|
+
readers: ReadonlySet<string>;
|
|
92
|
+
}
|
|
93
|
+
interface CompiledConfig {
|
|
94
|
+
members: ReadonlyMap<string, RoomMember>;
|
|
95
|
+
rooms: ReadonlyMap<string, CompiledRoom>;
|
|
96
|
+
}
|
|
97
|
+
export declare function defaultRoomAccessPath(): string;
|
|
98
|
+
/** Validate and compile a configuration object; throws a readable error. */
|
|
99
|
+
export declare function compileRoomAccessConfig(input: unknown): CompiledConfig;
|
|
100
|
+
export interface RoomAccessPolicyOptions {
|
|
101
|
+
/** Read this JSON file (default `$CODEBUDDY_FLEET_ROOMS_CONFIG` or `<CODEBUDDY_HOME>/fleet/rooms.json`). */
|
|
102
|
+
path?: string;
|
|
103
|
+
/** Use an in-memory configuration instead of a file (tests, embedding). */
|
|
104
|
+
config?: RoomAccessConfigInput;
|
|
105
|
+
/** Minimum delay between two file checks, in ms (finite, >= 0). */
|
|
106
|
+
reloadIntervalMs?: number;
|
|
107
|
+
}
|
|
108
|
+
export declare class RoomAccessPolicy {
|
|
109
|
+
private readonly path?;
|
|
110
|
+
private readonly reloadIntervalMs;
|
|
111
|
+
private compiled;
|
|
112
|
+
private loadedStamp?;
|
|
113
|
+
private lastCheckMs;
|
|
114
|
+
private lastProblem?;
|
|
115
|
+
constructor(options?: RoomAccessPolicyOptions);
|
|
116
|
+
private report;
|
|
117
|
+
private deny;
|
|
118
|
+
private refresh;
|
|
119
|
+
/** Frozen member record, or undefined. */
|
|
120
|
+
member(pubkey: string): RoomMember | undefined;
|
|
121
|
+
/** May this key act on a connection authenticated as `principalId`? */
|
|
122
|
+
admit(pubkey: string, principalId: string): {
|
|
123
|
+
ok: true;
|
|
124
|
+
member: RoomMember;
|
|
125
|
+
} | {
|
|
126
|
+
ok: false;
|
|
127
|
+
reason: string;
|
|
128
|
+
};
|
|
129
|
+
access(pubkey: string, room: string): RoomAccessLevel | undefined;
|
|
130
|
+
canRead(pubkey: string, room: string): boolean;
|
|
131
|
+
canWrite(pubkey: string, room: string): boolean;
|
|
132
|
+
roomsFor(pubkey: string): Array<{
|
|
133
|
+
room: string;
|
|
134
|
+
access: RoomAccessLevel;
|
|
135
|
+
description?: string;
|
|
136
|
+
}>;
|
|
137
|
+
}
|
|
138
|
+
export {};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fleet rooms — membership and access policy.
|
|
3
|
+
*
|
|
4
|
+
* Buzz makes channel membership "the only gate", enforced by the relay before
|
|
5
|
+
* any read or write (`ARCHITECTURE.md` §7, `crates/buzz-relay/src/handlers/req.rs`).
|
|
6
|
+
* Here the hub operator declares members (public keys) and rooms in one file:
|
|
7
|
+
*
|
|
8
|
+
* ```json
|
|
9
|
+
* {
|
|
10
|
+
* "version": 1,
|
|
11
|
+
* "members": { "<pubkey hex>": { "name": "peer-alpha", "principals": ["key:abc"] } },
|
|
12
|
+
* "rooms": { "general": { "members": ["<pubkey hex>"], "readers": ["<pubkey hex>"] } }
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* - `rooms.<id>.members` may read and write; `readers` may only read.
|
|
17
|
+
* - `principals` binds a key to WebSocket identities: when the field is ABSENT
|
|
18
|
+
* the key is accepted on any authenticated connection; when it is a list the
|
|
19
|
+
* connection principal must be in it; an EMPTY list refuses every connection.
|
|
20
|
+
*
|
|
21
|
+
* Fail-closed: a missing, unreadable or invalid file yields no member and no
|
|
22
|
+
* room. Refresh window: the file's `mtimeMs`/`ctimeMs`/size/inode are checked at
|
|
23
|
+
* most once per `reloadIntervalMs` (default 1000 ms), so a revocation applies
|
|
24
|
+
* to the first check after that window — including deliveries to subscriptions
|
|
25
|
+
* that are already open.
|
|
26
|
+
*
|
|
27
|
+
* @module fleet/rooms/room-access
|
|
28
|
+
*/
|
|
29
|
+
import fs from 'fs';
|
|
30
|
+
import { z } from 'zod';
|
|
31
|
+
import { getCodeBuddyPath } from '../../utils/codebuddy-home.js';
|
|
32
|
+
import { logger } from '../../utils/logger.js';
|
|
33
|
+
import { ROOM_ID_PATTERN } from './room-event.js';
|
|
34
|
+
export const MAX_ROOM_ACCESS_MEMBERS = 1_024;
|
|
35
|
+
export const MAX_ROOM_ACCESS_ROOMS = 1_024;
|
|
36
|
+
const MAX_CONFIG_BYTES = 1024 * 1024;
|
|
37
|
+
const pubkeySchema = z.string().regex(/^[0-9a-f]{64}$/);
|
|
38
|
+
const configSchema = z.object({
|
|
39
|
+
version: z.literal(1),
|
|
40
|
+
members: z.record(pubkeySchema, z.object({
|
|
41
|
+
name: z.string().min(1).max(64),
|
|
42
|
+
principals: z.array(z.string().min(1).max(256)).max(32).optional(),
|
|
43
|
+
}).strict()),
|
|
44
|
+
rooms: z.record(z.string().regex(ROOM_ID_PATTERN), z.object({
|
|
45
|
+
description: z.string().max(512).optional(),
|
|
46
|
+
members: z.array(pubkeySchema).max(MAX_ROOM_ACCESS_MEMBERS).default([]),
|
|
47
|
+
readers: z.array(pubkeySchema).max(MAX_ROOM_ACCESS_MEMBERS).default([]),
|
|
48
|
+
}).strict()),
|
|
49
|
+
}).strict();
|
|
50
|
+
const EMPTY_CONFIG = { members: new Map(), rooms: new Map() };
|
|
51
|
+
export function defaultRoomAccessPath() {
|
|
52
|
+
return process.env.CODEBUDDY_FLEET_ROOMS_CONFIG || getCodeBuddyPath('fleet', 'rooms.json');
|
|
53
|
+
}
|
|
54
|
+
/** Validate and compile a configuration object; throws a readable error. */
|
|
55
|
+
export function compileRoomAccessConfig(input) {
|
|
56
|
+
const parsed = configSchema.safeParse(input);
|
|
57
|
+
if (!parsed.success) {
|
|
58
|
+
const issue = parsed.error.issues[0];
|
|
59
|
+
throw new Error(`invalid rooms config at ${issue?.path.join('.') || '<root>'}: ${issue?.message ?? 'unknown'}`);
|
|
60
|
+
}
|
|
61
|
+
const memberEntries = Object.entries(parsed.data.members);
|
|
62
|
+
const roomEntries = Object.entries(parsed.data.rooms);
|
|
63
|
+
if (memberEntries.length > MAX_ROOM_ACCESS_MEMBERS) {
|
|
64
|
+
throw new Error(`invalid rooms config: more than ${MAX_ROOM_ACCESS_MEMBERS} members`);
|
|
65
|
+
}
|
|
66
|
+
if (roomEntries.length > MAX_ROOM_ACCESS_ROOMS) {
|
|
67
|
+
throw new Error(`invalid rooms config: more than ${MAX_ROOM_ACCESS_ROOMS} rooms`);
|
|
68
|
+
}
|
|
69
|
+
const members = new Map();
|
|
70
|
+
for (const [pubkey, member] of memberEntries) {
|
|
71
|
+
members.set(pubkey, Object.freeze({
|
|
72
|
+
pubkey,
|
|
73
|
+
name: member.name,
|
|
74
|
+
...(member.principals ? { principals: Object.freeze([...member.principals]) } : {}),
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
const rooms = new Map();
|
|
78
|
+
for (const [room, spec] of roomEntries) {
|
|
79
|
+
for (const pubkey of [...spec.members, ...spec.readers]) {
|
|
80
|
+
if (!members.has(pubkey)) {
|
|
81
|
+
throw new Error(`invalid rooms config: room ${room} lists an undeclared member ${pubkey.slice(0, 12)}…`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
rooms.set(room, {
|
|
85
|
+
...(spec.description ? { description: spec.description } : {}),
|
|
86
|
+
writers: new Set(spec.members),
|
|
87
|
+
readers: new Set([...spec.members, ...spec.readers]),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return { members, rooms };
|
|
91
|
+
}
|
|
92
|
+
export class RoomAccessPolicy {
|
|
93
|
+
path;
|
|
94
|
+
reloadIntervalMs;
|
|
95
|
+
compiled = EMPTY_CONFIG;
|
|
96
|
+
loadedStamp;
|
|
97
|
+
lastCheckMs = -Infinity;
|
|
98
|
+
lastProblem;
|
|
99
|
+
constructor(options = {}) {
|
|
100
|
+
const interval = options.reloadIntervalMs ?? 1_000;
|
|
101
|
+
if (typeof interval !== 'number' || !Number.isFinite(interval) || interval < 0) {
|
|
102
|
+
throw new RangeError('reloadIntervalMs must be a finite number >= 0');
|
|
103
|
+
}
|
|
104
|
+
this.reloadIntervalMs = interval;
|
|
105
|
+
if (options.config) {
|
|
106
|
+
this.compiled = compileRoomAccessConfig(options.config);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
this.path = options.path ?? defaultRoomAccessPath();
|
|
110
|
+
this.refresh(true);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
report(problem) {
|
|
114
|
+
if (problem && problem !== this.lastProblem) {
|
|
115
|
+
logger.warn(`[fleet-rooms] access policy denies everything: ${problem}`);
|
|
116
|
+
}
|
|
117
|
+
this.lastProblem = problem;
|
|
118
|
+
}
|
|
119
|
+
deny(problem) {
|
|
120
|
+
this.compiled = EMPTY_CONFIG;
|
|
121
|
+
this.report(problem);
|
|
122
|
+
}
|
|
123
|
+
refresh(force = false) {
|
|
124
|
+
if (!this.path)
|
|
125
|
+
return;
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
if (!force && now - this.lastCheckMs < this.reloadIntervalMs)
|
|
128
|
+
return;
|
|
129
|
+
this.lastCheckMs = now;
|
|
130
|
+
let stat;
|
|
131
|
+
try {
|
|
132
|
+
stat = fs.statSync(this.path);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
this.loadedStamp = undefined;
|
|
136
|
+
this.deny(`no rooms config at ${this.path}`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const stamp = `${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}:${stat.ino}`;
|
|
140
|
+
if (stamp === this.loadedStamp)
|
|
141
|
+
return;
|
|
142
|
+
this.loadedStamp = stamp;
|
|
143
|
+
if (stat.size > MAX_CONFIG_BYTES) {
|
|
144
|
+
this.deny(`rooms config larger than ${MAX_CONFIG_BYTES} bytes`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
this.compiled = compileRoomAccessConfig(JSON.parse(fs.readFileSync(this.path, 'utf8')));
|
|
149
|
+
this.report(undefined);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
this.deny(error instanceof Error ? error.message : String(error));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** Frozen member record, or undefined. */
|
|
156
|
+
member(pubkey) {
|
|
157
|
+
this.refresh();
|
|
158
|
+
return this.compiled.members.get(pubkey);
|
|
159
|
+
}
|
|
160
|
+
/** May this key act on a connection authenticated as `principalId`? */
|
|
161
|
+
admit(pubkey, principalId) {
|
|
162
|
+
const member = this.member(pubkey);
|
|
163
|
+
if (!member)
|
|
164
|
+
return { ok: false, reason: 'restricted: key is not a declared room member' };
|
|
165
|
+
if (member.principals !== undefined && !member.principals.includes(principalId)) {
|
|
166
|
+
return { ok: false, reason: 'restricted: key is not bound to this connection identity' };
|
|
167
|
+
}
|
|
168
|
+
return { ok: true, member };
|
|
169
|
+
}
|
|
170
|
+
access(pubkey, room) {
|
|
171
|
+
this.refresh();
|
|
172
|
+
const spec = this.compiled.rooms.get(room);
|
|
173
|
+
if (!spec || !this.compiled.members.has(pubkey))
|
|
174
|
+
return undefined;
|
|
175
|
+
if (spec.writers.has(pubkey))
|
|
176
|
+
return 'write';
|
|
177
|
+
return spec.readers.has(pubkey) ? 'read' : undefined;
|
|
178
|
+
}
|
|
179
|
+
canRead(pubkey, room) {
|
|
180
|
+
return this.access(pubkey, room) !== undefined;
|
|
181
|
+
}
|
|
182
|
+
canWrite(pubkey, room) {
|
|
183
|
+
return this.access(pubkey, room) === 'write';
|
|
184
|
+
}
|
|
185
|
+
roomsFor(pubkey) {
|
|
186
|
+
this.refresh();
|
|
187
|
+
const result = [];
|
|
188
|
+
for (const [room, spec] of this.compiled.rooms) {
|
|
189
|
+
const access = this.access(pubkey, room);
|
|
190
|
+
if (access)
|
|
191
|
+
result.push({ room, access, ...(spec.description ? { description: spec.description } : {}) });
|
|
192
|
+
}
|
|
193
|
+
return result.sort((a, b) => a.room.localeCompare(b.room));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=room-access.js.map
|