@hcu-lab.me/mcp-device 1.0.9 → 1.0.12
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/PRIVACY.md +21 -0
- package/README.md +58 -3
- package/SECURITY.md +67 -0
- package/config/revoked-approvals.json +1 -0
- package/control-plane/config/mcp-servers.example.toml +52 -0
- package/control-plane/config/mcp-servers.toml +52 -0
- package/control-plane/contracts/device-public-contract-v1.json +733 -0
- package/control-plane/scripts/GATEWAY-LICENSE.txt +7 -0
- package/control-plane/scripts/account-http.mjs +220 -0
- package/control-plane/scripts/account-store.mjs +368 -0
- package/control-plane/scripts/admin.mjs +68 -0
- package/control-plane/scripts/auth-session.mjs +411 -0
- package/control-plane/scripts/authenticated-mcp-wrapper.mjs +1281 -0
- package/control-plane/scripts/benchmark-command.mjs +9 -0
- package/control-plane/scripts/benchmark-mcp-catalog.mjs +174 -0
- package/control-plane/scripts/catalog-budget.mjs +87 -0
- package/control-plane/scripts/check-device-public-contract.mjs +20 -0
- package/control-plane/scripts/custom-tools/index.mjs +221 -0
- package/control-plane/scripts/custom-tools/path-utils.mjs +117 -0
- package/control-plane/scripts/custom-tools/response-utils.mjs +34 -0
- package/control-plane/scripts/dashboard-http.mjs +598 -0
- package/control-plane/scripts/device-access-policy.mjs +205 -0
- package/control-plane/scripts/device-admin.mjs +43 -0
- package/control-plane/scripts/device-audit.mjs +63 -0
- package/control-plane/scripts/device-broker.mjs +1461 -0
- package/control-plane/scripts/device-execution-tool-definitions.mjs +217 -0
- package/control-plane/scripts/device-inventory.mjs +128 -0
- package/control-plane/scripts/device-pairing-http.mjs +109 -0
- package/control-plane/scripts/device-pairing-store.mjs +245 -0
- package/control-plane/scripts/device-public-contract.mjs +165 -0
- package/control-plane/scripts/device-release.mjs +95 -0
- package/control-plane/scripts/device-secure-transport.mjs +128 -0
- package/control-plane/scripts/device-store.mjs +283 -0
- package/control-plane/scripts/device-usage.mjs +657 -0
- package/control-plane/scripts/embedded-device.mjs +149 -0
- package/control-plane/scripts/enduser-ui.mjs +45 -0
- package/control-plane/scripts/external-tool-broker.mjs +179 -0
- package/control-plane/scripts/oauth-authorization-server.mjs +589 -0
- package/control-plane/scripts/operator-dashboard-http.mjs +81 -0
- package/control-plane/scripts/project-inspection.mjs +227 -0
- package/control-plane/scripts/projects/project-id.mjs +69 -0
- package/control-plane/scripts/projects/trusted-roots-projects.mjs +375 -0
- package/control-plane/scripts/prompts/index.mjs +60 -0
- package/control-plane/scripts/remote-process-sessions.mjs +78 -0
- package/control-plane/scripts/remote-tool-result.mjs +9 -0
- package/control-plane/scripts/resources/index.mjs +166 -0
- package/control-plane/scripts/runtime-profile.mjs +54 -0
- package/control-plane/scripts/shell-tool-descriptor.mjs +17 -0
- package/control-plane/scripts/skill-source-safety.mjs +46 -0
- package/control-plane/scripts/skills/README.md +70 -0
- package/control-plane/scripts/skills/_upstream_licenses/anthropic-THIRD_PARTY_NOTICES.md +405 -0
- package/control-plane/scripts/skills/_upstream_licenses/hallmark-LICENSE.txt +21 -0
- package/control-plane/scripts/skills/_upstream_licenses/ponytail-LICENSE.txt +21 -0
- package/control-plane/scripts/skills/_upstream_licenses/stitch-skills-LICENSE.txt +201 -0
- package/control-plane/scripts/skills/_upstream_licenses/superpowers-LICENSE.txt +21 -0
- package/control-plane/scripts/skills/algorithmic-art/LICENSE.txt +202 -0
- package/control-plane/scripts/skills/algorithmic-art/SKILL.md +405 -0
- package/control-plane/scripts/skills/algorithmic-art/templates/generator_template.js +223 -0
- package/control-plane/scripts/skills/algorithmic-art/templates/viewer.html +599 -0
- package/control-plane/scripts/skills/brainstorming/SKILL.md +285 -0
- package/control-plane/scripts/skills/brainstorming/scripts/frame-template.html +213 -0
- package/control-plane/scripts/skills/brainstorming/scripts/helper.js +167 -0
- package/control-plane/scripts/skills/brainstorming/scripts/server.cjs +723 -0
- package/control-plane/scripts/skills/brainstorming/scripts/start-server.sh +209 -0
- package/control-plane/scripts/skills/brainstorming/scripts/stop-server.sh +120 -0
- package/control-plane/scripts/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
- package/control-plane/scripts/skills/brainstorming/visual-companion.md +299 -0
- package/control-plane/scripts/skills/brand-guidelines/LICENSE.txt +202 -0
- package/control-plane/scripts/skills/brand-guidelines/SKILL.md +73 -0
- package/control-plane/scripts/skills/claude-api/LICENSE.txt +202 -0
- package/control-plane/scripts/skills/claude-api/SKILL.md +570 -0
- package/control-plane/scripts/skills/claude-api/csharp/claude-api/README.md +362 -0
- package/control-plane/scripts/skills/claude-api/csharp/claude-api/batches.md +14 -0
- package/control-plane/scripts/skills/claude-api/csharp/claude-api/files-api.md +25 -0
- package/control-plane/scripts/skills/claude-api/csharp/claude-api/streaming.md +28 -0
- package/control-plane/scripts/skills/claude-api/csharp/claude-api/tool-use.md +164 -0
- package/control-plane/scripts/skills/claude-api/curl/examples.md +256 -0
- package/control-plane/scripts/skills/claude-api/curl/managed-agents.md +371 -0
- package/control-plane/scripts/skills/claude-api/go/claude-api/README.md +188 -0
- package/control-plane/scripts/skills/claude-api/go/claude-api/files-api.md +23 -0
- package/control-plane/scripts/skills/claude-api/go/claude-api/streaming.md +43 -0
- package/control-plane/scripts/skills/claude-api/go/claude-api/tool-use.md +220 -0
- package/control-plane/scripts/skills/claude-api/go/managed-agents/README.md +564 -0
- package/control-plane/scripts/skills/claude-api/java/claude-api/README.md +238 -0
- package/control-plane/scripts/skills/claude-api/java/claude-api/files-api.md +27 -0
- package/control-plane/scripts/skills/claude-api/java/claude-api/streaming.md +24 -0
- package/control-plane/scripts/skills/claude-api/java/claude-api/tool-use.md +228 -0
- package/control-plane/scripts/skills/claude-api/java/managed-agents/README.md +443 -0
- package/control-plane/scripts/skills/claude-api/php/claude-api/README.md +174 -0
- package/control-plane/scripts/skills/claude-api/php/claude-api/batches.md +15 -0
- package/control-plane/scripts/skills/claude-api/php/claude-api/files-api.md +13 -0
- package/control-plane/scripts/skills/claude-api/php/claude-api/streaming.md +27 -0
- package/control-plane/scripts/skills/claude-api/php/claude-api/tool-use.md +253 -0
- package/control-plane/scripts/skills/claude-api/php/managed-agents/README.md +443 -0
- package/control-plane/scripts/skills/claude-api/python/claude-api/README.md +565 -0
- package/control-plane/scripts/skills/claude-api/python/claude-api/batches.md +198 -0
- package/control-plane/scripts/skills/claude-api/python/claude-api/files-api.md +170 -0
- package/control-plane/scripts/skills/claude-api/python/claude-api/sdk-upgrade.md +286 -0
- package/control-plane/scripts/skills/claude-api/python/claude-api/streaming.md +179 -0
- package/control-plane/scripts/skills/claude-api/python/claude-api/tool-use.md +627 -0
- package/control-plane/scripts/skills/claude-api/python/managed-agents/README.md +335 -0
- package/control-plane/scripts/skills/claude-api/ruby/claude-api/README.md +134 -0
- package/control-plane/scripts/skills/claude-api/ruby/claude-api/streaming.md +16 -0
- package/control-plane/scripts/skills/claude-api/ruby/claude-api/tool-use.md +41 -0
- package/control-plane/scripts/skills/claude-api/ruby/managed-agents/README.md +394 -0
- package/control-plane/scripts/skills/claude-api/shared/admin-api.md +179 -0
- package/control-plane/scripts/skills/claude-api/shared/agent-design.md +101 -0
- package/control-plane/scripts/skills/claude-api/shared/anthropic-cli.md +269 -0
- package/control-plane/scripts/skills/claude-api/shared/claude-platform-on-aws.md +59 -0
- package/control-plane/scripts/skills/claude-api/shared/cost-optimization.md +233 -0
- package/control-plane/scripts/skills/claude-api/shared/error-codes.md +262 -0
- package/control-plane/scripts/skills/claude-api/shared/live-sources.md +166 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-api-reference.md +455 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-client-patterns.md +220 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-core.md +359 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-environments.md +226 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-events.md +290 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-memory.md +201 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-multiagent.md +260 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-onboarding.md +82 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-outcomes.md +108 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-overview.md +77 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-scheduled-deployments.md +156 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-self-hosted-sandboxes.md +282 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-tools.md +504 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-webhooks.md +143 -0
- package/control-plane/scripts/skills/claude-api/shared/model-migration.md +1872 -0
- package/control-plane/scripts/skills/claude-api/shared/models.md +142 -0
- package/control-plane/scripts/skills/claude-api/shared/platform-availability.md +52 -0
- package/control-plane/scripts/skills/claude-api/shared/prompt-audit.md +223 -0
- package/control-plane/scripts/skills/claude-api/shared/prompt-caching.md +292 -0
- package/control-plane/scripts/skills/claude-api/shared/token-counting.md +56 -0
- package/control-plane/scripts/skills/claude-api/shared/tool-use-concepts.md +526 -0
- package/control-plane/scripts/skills/claude-api/typescript/claude-api/README.md +405 -0
- package/control-plane/scripts/skills/claude-api/typescript/claude-api/batches.md +106 -0
- package/control-plane/scripts/skills/claude-api/typescript/claude-api/files-api.md +98 -0
- package/control-plane/scripts/skills/claude-api/typescript/claude-api/streaming.md +178 -0
- package/control-plane/scripts/skills/claude-api/typescript/claude-api/tool-use.md +586 -0
- package/control-plane/scripts/skills/claude-api/typescript/managed-agents/README.md +359 -0
- package/control-plane/scripts/skills/dispatching-parallel-agents/SKILL.md +167 -0
- package/control-plane/scripts/skills/enhance-prompt/README.md +34 -0
- package/control-plane/scripts/skills/enhance-prompt/SKILL.md +204 -0
- package/control-plane/scripts/skills/enhance-prompt/references/KEYWORDS.md +114 -0
- package/control-plane/scripts/skills/executing-plans/SKILL.md +371 -0
- package/control-plane/scripts/skills/executing-plans/scripts/task-done +52 -0
- package/control-plane/scripts/skills/executing-plans/scripts/task-start +28 -0
- package/control-plane/scripts/skills/extract-design-md/SKILL.md +358 -0
- package/control-plane/scripts/skills/extract-design-md/examples/DESIGN.md +210 -0
- package/control-plane/scripts/skills/extract-design-md/references/angular.md +122 -0
- package/control-plane/scripts/skills/extract-design-md/references/plain-css.md +161 -0
- package/control-plane/scripts/skills/extract-design-md/references/react-tailwind.md +157 -0
- package/control-plane/scripts/skills/extract-design-md/references/svelte.md +101 -0
- package/control-plane/scripts/skills/extract-design-md/references/vue.md +107 -0
- package/control-plane/scripts/skills/finishing-a-development-branch/SKILL.md +225 -0
- package/control-plane/scripts/skills/frontend-design/LICENSE.txt +177 -0
- package/control-plane/scripts/skills/frontend-design/SKILL.md +71 -0
- package/control-plane/scripts/skills/hallmark/SKILL.md +558 -0
- package/control-plane/scripts/skills/hallmark/references/anti-patterns.md +418 -0
- package/control-plane/scripts/skills/hallmark/references/assets.md +406 -0
- package/control-plane/scripts/skills/hallmark/references/color.md +95 -0
- package/control-plane/scripts/skills/hallmark/references/component-cookbook.md +263 -0
- package/control-plane/scripts/skills/hallmark/references/components/c1-outlined-chip.md +12 -0
- package/control-plane/scripts/skills/hallmark/references/components/c2-inline-form-as-cta.md +16 -0
- package/control-plane/scripts/skills/hallmark/references/components/c3-typographic-link.md +8 -0
- package/control-plane/scripts/skills/hallmark/references/components/c4-sticky-bottom-bar.md +16 -0
- package/control-plane/scripts/skills/hallmark/references/components/f1-bento-grid.md +20 -0
- package/control-plane/scripts/skills/hallmark/references/components/f2-sticky-scroll-stack.md +20 -0
- package/control-plane/scripts/skills/hallmark/references/components/f3-tabular-spec-sheet.md +11 -0
- package/control-plane/scripts/skills/hallmark/references/components/f4-step-sequence.md +11 -0
- package/control-plane/scripts/skills/hallmark/references/components/f5-annotated-screenshot.md +11 -0
- package/control-plane/scripts/skills/hallmark/references/components/f6-product-card-grid.md +41 -0
- package/control-plane/scripts/skills/hallmark/references/components/ft1-mast-headed.md +13 -0
- package/control-plane/scripts/skills/hallmark/references/components/ft2-inline-rule-single-line.md +10 -0
- package/control-plane/scripts/skills/hallmark/references/components/ft3-index-style-category-list.md +12 -0
- package/control-plane/scripts/skills/hallmark/references/components/ft4-dense-typographic.md +10 -0
- package/control-plane/scripts/skills/hallmark/references/components/ft5-statement.md +21 -0
- package/control-plane/scripts/skills/hallmark/references/components/ft6-letter-close.md +19 -0
- package/control-plane/scripts/skills/hallmark/references/components/ft7-newsletter-first.md +27 -0
- package/control-plane/scripts/skills/hallmark/references/components/ft8-marquee-scroll.md +25 -0
- package/control-plane/scripts/skills/hallmark/references/components/h1-marquee.md +15 -0
- package/control-plane/scripts/skills/hallmark/references/components/h2-split-diptych.md +15 -0
- package/control-plane/scripts/skills/hallmark/references/components/h3-quote-led.md +11 -0
- package/control-plane/scripts/skills/hallmark/references/components/h4-stat-led.md +14 -0
- package/control-plane/scripts/skills/hallmark/references/components/h5-letter-hero.md +11 -0
- package/control-plane/scripts/skills/hallmark/references/components/h6-photographic-fold.md +16 -0
- package/control-plane/scripts/skills/hallmark/references/components/h7-demo-video-clipped-by-viewport-edge.md +27 -0
- package/control-plane/scripts/skills/hallmark/references/components/h8-mockup-split-browser-framed.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/components/h9-custom-illustration-centerpiece.md +27 -0
- package/control-plane/scripts/skills/hallmark/references/components/n1-wordmark-2-links.md +12 -0
- package/control-plane/scripts/skills/hallmark/references/components/n10-floating-on-scroll-morph.md +19 -0
- package/control-plane/scripts/skills/hallmark/references/components/n11-mega-menu.md +40 -0
- package/control-plane/scripts/skills/hallmark/references/components/n12-banner-retract.md +34 -0
- package/control-plane/scripts/skills/hallmark/references/components/n13-inline-cmdk-pill.md +39 -0
- package/control-plane/scripts/skills/hallmark/references/components/n1b-saas-three-section.md +35 -0
- package/control-plane/scripts/skills/hallmark/references/components/n2-floating-chip.md +14 -0
- package/control-plane/scripts/skills/hallmark/references/components/n3-side-rail.md +14 -0
- package/control-plane/scripts/skills/hallmark/references/components/n4-hidden-behind-k.md +9 -0
- package/control-plane/scripts/skills/hallmark/references/components/n5-floating-pill.md +28 -0
- package/control-plane/scripts/skills/hallmark/references/components/n6-newspaper-masthead.md +24 -0
- package/control-plane/scripts/skills/hallmark/references/components/n7-brutal-slab.md +22 -0
- package/control-plane/scripts/skills/hallmark/references/components/n8-terminal-command.md +21 -0
- package/control-plane/scripts/skills/hallmark/references/components/n9-edge-aligned-minimal.md +17 -0
- package/control-plane/scripts/skills/hallmark/references/components/s1-left-margin-numbered.md +15 -0
- package/control-plane/scripts/skills/hallmark/references/components/s2-hanging.md +13 -0
- package/control-plane/scripts/skills/hallmark/references/components/s3-sticky-pinned.md +19 -0
- package/control-plane/scripts/skills/hallmark/references/components/s4-inline-no-break.md +11 -0
- package/control-plane/scripts/skills/hallmark/references/components/s5-bottom-anchored.md +13 -0
- package/control-plane/scripts/skills/hallmark/references/components/t1-pull-quote-with-marginalia.md +12 -0
- package/control-plane/scripts/skills/hallmark/references/components/t2-logo-wall-hairline.md +19 -0
- package/control-plane/scripts/skills/hallmark/references/components/t3-single-huge-quote.md +11 -0
- package/control-plane/scripts/skills/hallmark/references/components/t4-numbered-stat-strip.md +14 -0
- package/control-plane/scripts/skills/hallmark/references/contract.md +25 -0
- package/control-plane/scripts/skills/hallmark/references/copy.md +182 -0
- package/control-plane/scripts/skills/hallmark/references/custom-craft.md +626 -0
- package/control-plane/scripts/skills/hallmark/references/custom-theme.md +367 -0
- package/control-plane/scripts/skills/hallmark/references/design-md.md +116 -0
- package/control-plane/scripts/skills/hallmark/references/export-formats.md +329 -0
- package/control-plane/scripts/skills/hallmark/references/floating-nav.md +89 -0
- package/control-plane/scripts/skills/hallmark/references/genres/atmospheric.md +69 -0
- package/control-plane/scripts/skills/hallmark/references/genres/editorial.md +72 -0
- package/control-plane/scripts/skills/hallmark/references/genres/modern-minimal.md +71 -0
- package/control-plane/scripts/skills/hallmark/references/genres/playful.md +70 -0
- package/control-plane/scripts/skills/hallmark/references/hero-enrichment.md +475 -0
- package/control-plane/scripts/skills/hallmark/references/imagery-kit.md +170 -0
- package/control-plane/scripts/skills/hallmark/references/interaction-and-states.md +208 -0
- package/control-plane/scripts/skills/hallmark/references/layout-and-space.md +112 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/01-bento-grid.md +35 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/02-long-document.md +34 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/03-marquee-hero.md +31 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/04-stat-led.md +34 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/05-workbench.md +32 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/06-conversational-faq.md +33 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/07-manifesto.md +32 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/08-photographic.md +34 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/09-quote-led.md +32 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/10-specimen.md +32 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/11-catalogue.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/12-letter.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/13-index-first.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/14-narrative-workflow.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/15-split-studio.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/16-feature-stack.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/17-type-specimen.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/18-portfolio-grid.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/19-map-diagram.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/20-ecosystem-index.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/21-component-playground.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures.md +89 -0
- package/control-plane/scripts/skills/hallmark/references/microinteractions.md +258 -0
- package/control-plane/scripts/skills/hallmark/references/motion.md +109 -0
- package/control-plane/scripts/skills/hallmark/references/preview-examples.md +49 -0
- package/control-plane/scripts/skills/hallmark/references/responsive.md +138 -0
- package/control-plane/scripts/skills/hallmark/references/slop-test.md +192 -0
- package/control-plane/scripts/skills/hallmark/references/structure.md +160 -0
- package/control-plane/scripts/skills/hallmark/references/study.md +509 -0
- package/control-plane/scripts/skills/hallmark/references/themes/carnival.md +301 -0
- package/control-plane/scripts/skills/hallmark/references/themes/cobalt.md +146 -0
- package/control-plane/scripts/skills/hallmark/references/themes/grid.md +161 -0
- package/control-plane/scripts/skills/hallmark/references/themes/hum.md +403 -0
- package/control-plane/scripts/skills/hallmark/references/themes/lumen.md +478 -0
- package/control-plane/scripts/skills/hallmark/references/typography.md +243 -0
- package/control-plane/scripts/skills/hallmark/references/upstream/recipes.md +186 -0
- package/control-plane/scripts/skills/hallmark/references/upstream/study-examples.md +176 -0
- package/control-plane/scripts/skills/hallmark/references/upstream/tokens.css +1221 -0
- package/control-plane/scripts/skills/hallmark/references/verbs/audit.md +25 -0
- package/control-plane/scripts/skills/hallmark/references/verbs/redesign.md +269 -0
- package/control-plane/scripts/skills/human-comms/SKILL.md +29 -0
- package/control-plane/scripts/skills/index.mjs +432 -0
- package/control-plane/scripts/skills/internal-comms/LICENSE.txt +202 -0
- package/control-plane/scripts/skills/internal-comms/SKILL.md +32 -0
- package/control-plane/scripts/skills/internal-comms/examples/3p-updates.md +47 -0
- package/control-plane/scripts/skills/internal-comms/examples/company-newsletter.md +65 -0
- package/control-plane/scripts/skills/internal-comms/examples/faq-answers.md +30 -0
- package/control-plane/scripts/skills/internal-comms/examples/general-comms.md +16 -0
- package/control-plane/scripts/skills/mcp-adapter.mjs +94 -0
- package/control-plane/scripts/skills/mcp-builder/LICENSE.txt +202 -0
- package/control-plane/scripts/skills/mcp-builder/SKILL.md +236 -0
- package/control-plane/scripts/skills/mcp-builder/reference/evaluation.md +602 -0
- package/control-plane/scripts/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
- package/control-plane/scripts/skills/mcp-builder/reference/node_mcp_server.md +970 -0
- package/control-plane/scripts/skills/mcp-builder/reference/python_mcp_server.md +719 -0
- package/control-plane/scripts/skills/mcp-builder/scripts/connections.py +151 -0
- package/control-plane/scripts/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/control-plane/scripts/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/control-plane/scripts/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/control-plane/scripts/skills/ponytail/SKILL.md +120 -0
- package/control-plane/scripts/skills/ponytail-audit/SKILL.md +41 -0
- package/control-plane/scripts/skills/ponytail-debt/SKILL.md +44 -0
- package/control-plane/scripts/skills/ponytail-gain/SKILL.md +50 -0
- package/control-plane/scripts/skills/ponytail-help/SKILL.md +71 -0
- package/control-plane/scripts/skills/ponytail-review/SKILL.md +57 -0
- package/control-plane/scripts/skills/receiving-code-review/SKILL.md +205 -0
- package/control-plane/scripts/skills/requesting-code-review/SKILL.md +95 -0
- package/control-plane/scripts/skills/requesting-code-review/code-reviewer.md +198 -0
- package/control-plane/scripts/skills/skill-creator/LICENSE.txt +202 -0
- package/control-plane/scripts/skills/skill-creator/SKILL.md +485 -0
- package/control-plane/scripts/skills/skill-creator/agents/analyzer.md +274 -0
- package/control-plane/scripts/skills/skill-creator/agents/comparator.md +202 -0
- package/control-plane/scripts/skills/skill-creator/agents/grader.md +223 -0
- package/control-plane/scripts/skills/skill-creator/assets/eval_review.html +146 -0
- package/control-plane/scripts/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/control-plane/scripts/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/control-plane/scripts/skills/skill-creator/references/schemas.md +430 -0
- package/control-plane/scripts/skills/skill-creator/scripts/__init__.py +0 -0
- package/control-plane/scripts/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/control-plane/scripts/skills/skill-creator/scripts/generate_report.py +326 -0
- package/control-plane/scripts/skills/skill-creator/scripts/improve_description.py +247 -0
- package/control-plane/scripts/skills/skill-creator/scripts/package_skill.py +136 -0
- package/control-plane/scripts/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/control-plane/scripts/skills/skill-creator/scripts/run_eval.py +310 -0
- package/control-plane/scripts/skills/skill-creator/scripts/run_loop.py +328 -0
- package/control-plane/scripts/skills/skill-creator/scripts/utils.py +47 -0
- package/control-plane/scripts/skills/slack-gif-creator/LICENSE.txt +202 -0
- package/control-plane/scripts/skills/slack-gif-creator/SKILL.md +254 -0
- package/control-plane/scripts/skills/slack-gif-creator/core/easing.py +234 -0
- package/control-plane/scripts/skills/slack-gif-creator/core/frame_composer.py +176 -0
- package/control-plane/scripts/skills/slack-gif-creator/core/gif_builder.py +269 -0
- package/control-plane/scripts/skills/slack-gif-creator/core/validators.py +136 -0
- package/control-plane/scripts/skills/slack-gif-creator/requirements.txt +4 -0
- package/control-plane/scripts/skills/sources.json +401 -0
- package/control-plane/scripts/skills/sources.lock.json +416 -0
- package/control-plane/scripts/skills/subagent-driven-development/SKILL.md +570 -0
- package/control-plane/scripts/skills/subagent-driven-development/implementer-prompt.md +154 -0
- package/control-plane/scripts/skills/subagent-driven-development/re-review-prompt.md +115 -0
- package/control-plane/scripts/skills/subagent-driven-development/scripts/review-package +53 -0
- package/control-plane/scripts/skills/subagent-driven-development/scripts/sdd-workspace +82 -0
- package/control-plane/scripts/skills/subagent-driven-development/scripts/task-brief +43 -0
- package/control-plane/scripts/skills/subagent-driven-development/task-reviewer-prompt.md +207 -0
- package/control-plane/scripts/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/control-plane/scripts/skills/systematic-debugging/SKILL.md +283 -0
- package/control-plane/scripts/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/control-plane/scripts/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/control-plane/scripts/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/control-plane/scripts/skills/systematic-debugging/find-polluter.sh +72 -0
- package/control-plane/scripts/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/control-plane/scripts/skills/systematic-debugging/test-academic.md +14 -0
- package/control-plane/scripts/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/control-plane/scripts/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/control-plane/scripts/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/control-plane/scripts/skills/test-driven-development/SKILL.md +330 -0
- package/control-plane/scripts/skills/test-driven-development/writing-good-tests.md +198 -0
- package/control-plane/scripts/skills/theme-factory/LICENSE.txt +202 -0
- package/control-plane/scripts/skills/theme-factory/SKILL.md +59 -0
- package/control-plane/scripts/skills/theme-factory/theme-showcase.pdf +0 -0
- package/control-plane/scripts/skills/theme-factory/themes/arctic-frost.md +19 -0
- package/control-plane/scripts/skills/theme-factory/themes/botanical-garden.md +19 -0
- package/control-plane/scripts/skills/theme-factory/themes/desert-rose.md +19 -0
- package/control-plane/scripts/skills/theme-factory/themes/forest-canopy.md +19 -0
- package/control-plane/scripts/skills/theme-factory/themes/golden-hour.md +19 -0
- package/control-plane/scripts/skills/theme-factory/themes/midnight-galaxy.md +19 -0
- package/control-plane/scripts/skills/theme-factory/themes/modern-minimalist.md +19 -0
- package/control-plane/scripts/skills/theme-factory/themes/ocean-depths.md +19 -0
- package/control-plane/scripts/skills/theme-factory/themes/sunset-boulevard.md +19 -0
- package/control-plane/scripts/skills/theme-factory/themes/tech-innovation.md +19 -0
- package/control-plane/scripts/skills/using-git-worktrees/SKILL.md +167 -0
- package/control-plane/scripts/skills/verification-before-completion/SKILL.md +120 -0
- package/control-plane/scripts/skills/web-artifacts-builder/LICENSE.txt +202 -0
- package/control-plane/scripts/skills/web-artifacts-builder/SKILL.md +74 -0
- package/control-plane/scripts/skills/web-artifacts-builder/scripts/bundle-artifact.sh +54 -0
- package/control-plane/scripts/skills/web-artifacts-builder/scripts/init-artifact.sh +322 -0
- package/control-plane/scripts/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/control-plane/scripts/skills/webapp-testing/LICENSE.txt +202 -0
- package/control-plane/scripts/skills/webapp-testing/SKILL.md +96 -0
- package/control-plane/scripts/skills/webapp-testing/examples/console_logging.py +35 -0
- package/control-plane/scripts/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/control-plane/scripts/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/control-plane/scripts/skills/webapp-testing/scripts/with_server.py +106 -0
- package/control-plane/scripts/skills/writing-plans/SKILL.md +192 -0
- package/control-plane/scripts/skills/writing-plans/plan-document-reviewer-prompt.md +49 -0
- package/control-plane/scripts/skills/writing-skills/SKILL.md +681 -0
- package/control-plane/scripts/skills/writing-skills/anthropic-best-practices.md +1150 -0
- package/control-plane/scripts/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
- package/control-plane/scripts/skills/writing-skills/graphviz-conventions.dot +172 -0
- package/control-plane/scripts/skills/writing-skills/persuasion-principles.md +187 -0
- package/control-plane/scripts/skills/writing-skills/render-graphs.js +169 -0
- package/control-plane/scripts/skills/writing-skills/testing-skills-with-subagents.md +384 -0
- package/control-plane/scripts/smoke-mcp-schemas.mjs +30 -0
- package/control-plane/scripts/surface-config.mjs +45 -0
- package/control-plane/scripts/sync-skills.mjs +370 -0
- package/control-plane/scripts/tool-metrics.mjs +74 -0
- package/control-plane/scripts/tool-risk.mjs +156 -0
- package/control-plane/scripts/tool-surface-stability.mjs +38 -0
- package/control-plane/scripts/upstreams/catalog-cache.mjs +28 -0
- package/control-plane/scripts/upstreams/config.mjs +158 -0
- package/control-plane/scripts/upstreams/diagnostics.mjs +21 -0
- package/control-plane/scripts/upstreams/http-client.mjs +67 -0
- package/control-plane/scripts/upstreams/manager.mjs +535 -0
- package/control-plane/scripts/upstreams/names.mjs +55 -0
- package/control-plane/scripts/upstreams/presets.mjs +60 -0
- package/control-plane/scripts/upstreams/resource-uri.mjs +29 -0
- package/control-plane/scripts/upstreams/stdio-client.mjs +73 -0
- package/control-plane/scripts/workspace-registry.mjs +147 -0
- package/dist/context/authorization.d.ts +5 -0
- package/dist/context/authorization.js +70 -0
- package/dist/context/capture.d.ts +46 -0
- package/dist/context/capture.js +187 -0
- package/dist/context/cli.d.ts +47 -0
- package/dist/context/cli.js +287 -0
- package/dist/context/device-adapter.d.ts +102 -0
- package/dist/context/device-adapter.js +79 -0
- package/dist/context/gateway.d.ts +354 -0
- package/dist/context/gateway.js +50 -0
- package/dist/context/graph.d.ts +63 -0
- package/dist/context/graph.js +214 -0
- package/dist/context/importer.d.ts +8 -0
- package/dist/context/importer.js +69 -0
- package/dist/context/indexer.d.ts +14 -0
- package/dist/context/indexer.js +57 -0
- package/dist/context/migrations/index.d.ts +4 -0
- package/dist/context/migrations/index.js +87 -0
- package/dist/context/redact.d.ts +15 -0
- package/dist/context/redact.js +43 -0
- package/dist/context/repositories.d.ts +9 -0
- package/dist/context/repositories.js +45 -0
- package/dist/context/search.d.ts +63 -0
- package/dist/context/search.js +207 -0
- package/dist/context/service.d.ts +278 -0
- package/dist/context/service.js +160 -0
- package/dist/context/store.d.ts +134 -0
- package/dist/context/store.js +722 -0
- package/dist/context/text.d.ts +4 -0
- package/dist/context/text.js +14 -0
- package/dist/context/tool-contract.d.ts +534 -0
- package/dist/context/tool-contract.js +45 -0
- package/dist/device/core.d.ts +29 -0
- package/dist/device/core.js +66 -0
- package/dist/device/device.d.ts +5 -0
- package/dist/device/device.js +94 -37
- package/dist/device/gateway-channel.d.ts +8 -1
- package/dist/device/gateway-channel.js +51 -3
- package/dist/device/gateway-tool-adapter.d.ts +11 -3
- package/dist/device/gateway-tool-adapter.js +141 -98
- package/dist/device/official-trust.d.ts +12 -0
- package/dist/device/official-trust.js +25 -1
- package/dist/device/runtime-owner.js +9 -1
- package/dist/distribution.d.ts +4 -0
- package/dist/distribution.js +8 -0
- package/dist/handlers/filesystem-handlers.js +53 -37
- package/dist/handlers/search-handlers.d.ts +1 -0
- package/dist/handlers/search-handlers.js +51 -1
- package/dist/log-palace/cli.d.ts +7 -0
- package/dist/log-palace/cli.js +89 -0
- package/dist/log-palace/formats.d.ts +26 -0
- package/dist/log-palace/formats.js +126 -0
- package/dist/log-palace/store.d.ts +129 -0
- package/dist/log-palace/store.js +856 -0
- package/dist/mcp-device.d.ts +1 -1
- package/dist/mcp-device.js +28 -6
- package/dist/npm-scripts/remote.js +1 -0
- package/dist/platform/activity-journal.d.ts +16 -0
- package/dist/platform/activity-journal.js +68 -0
- package/dist/platform/cli.d.ts +1 -0
- package/dist/platform/cli.js +300 -0
- package/dist/platform/coordination-lock.d.ts +45 -0
- package/dist/platform/coordination-lock.js +233 -0
- package/dist/platform/device-runtime.d.ts +16 -0
- package/dist/platform/device-runtime.js +83 -0
- package/dist/platform/knowledge-contract.d.ts +46 -0
- package/dist/platform/knowledge-contract.js +1 -0
- package/dist/platform/knowledge-mcp.d.ts +29 -0
- package/dist/platform/knowledge-mcp.js +95 -0
- package/dist/platform/knowledge-router.d.ts +64 -0
- package/dist/platform/knowledge-router.js +320 -0
- package/dist/platform/plugin-child.d.ts +1 -0
- package/dist/platform/plugin-child.js +115 -0
- package/dist/platform/plugin-contract.d.ts +76 -0
- package/dist/platform/plugin-contract.js +53 -0
- package/dist/platform/plugin-driver.d.ts +8 -0
- package/dist/platform/plugin-driver.js +443 -0
- package/dist/platform/plugin-host.d.ts +88 -0
- package/dist/platform/plugin-host.js +817 -0
- package/dist/platform/plugin-policy.d.ts +3 -0
- package/dist/platform/plugin-policy.js +29 -0
- package/dist/platform/plugin-services.d.ts +14 -0
- package/dist/platform/plugin-services.js +32 -0
- package/dist/platform/process-identity.d.ts +9 -0
- package/dist/platform/process-identity.js +68 -0
- package/dist/platform/runtime-lease.d.ts +4 -0
- package/dist/platform/runtime-lease.js +58 -0
- package/dist/platform/runtime-root.d.ts +9 -0
- package/dist/platform/runtime-root.js +61 -0
- package/dist/platform/state-backup.d.ts +92 -0
- package/dist/platform/state-backup.js +399 -0
- package/dist/plugins/reference/plugin.d.ts +3 -0
- package/dist/plugins/reference/plugin.js +13 -0
- package/dist/search-manager.d.ts +6 -0
- package/dist/search-manager.js +123 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +7 -3
- package/dist/terminal-manager.d.ts +16 -0
- package/dist/terminal-manager.js +137 -42
- package/dist/tool-dispatcher.d.ts +11 -0
- package/dist/tool-dispatcher.js +228 -0
- package/dist/tools/edit.js +72 -1
- package/dist/tools/filesystem.d.ts +1 -1
- package/dist/tools/filesystem.js +2 -2
- package/dist/tools/improved-process-tools.js +91 -108
- package/dist/tools/schemas.d.ts +94 -2
- package/dist/tools/schemas.js +20 -6
- package/dist/types.d.ts +8 -0
- package/dist/utils/output-budget.d.ts +6 -0
- package/dist/utils/output-budget.js +15 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +29 -6
- package/skills/mcp-device-context/SKILL.md +40 -0
|
@@ -0,0 +1,1461 @@
|
|
|
1
|
+
import { createHash, createPublicKey, randomBytes, randomUUID, timingSafeEqual, verify } from 'node:crypto';
|
|
2
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
3
|
+
import {
|
|
4
|
+
DEVICE_INNER_TLS_SUBPROTOCOL,
|
|
5
|
+
createJsonFrameParser,
|
|
6
|
+
createServerInnerTls,
|
|
7
|
+
encodeJsonFrame
|
|
8
|
+
} from './device-secure-transport.mjs';
|
|
9
|
+
import { compareStableVersions, minimumSelfUpdateVersion, normalizeStableVersion, supportsDeviceSelfUpdate } from './device-release.mjs';
|
|
10
|
+
|
|
11
|
+
export const DEVICE_PROTOCOL_VERSION = 1;
|
|
12
|
+
export const DEVICE_PROTOCOL_VERSION_V2 = 2;
|
|
13
|
+
const DEVICE_AUTH_EXPORTER_LABEL = 'EXPERIMENTAL-HCU-MCP-DEVICE-AUTH-V2';
|
|
14
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 10000;
|
|
15
|
+
const DEFAULT_HELLO_TIMEOUT_MS = 5000;
|
|
16
|
+
const DEFAULT_UPDATE_TIMEOUT_MS = 15 * 60 * 1000;
|
|
17
|
+
const MAX_MESSAGE_BYTES = 64 * 1024;
|
|
18
|
+
const DEVICE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
19
|
+
|
|
20
|
+
export function buildDeviceAuthChallenge({ deviceId, nonce }) {
|
|
21
|
+
return Buffer.from(`mcp-device-auth-v1\n${deviceId}\n${nonce}`, 'utf8');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DEVICE_AUTH_V2_MODE = Object.freeze({ reconnect: 0, enroll: 1, pair: 2 });
|
|
25
|
+
|
|
26
|
+
function fixed32(value, name) {
|
|
27
|
+
const bytes = Buffer.isBuffer(value) ? value : Buffer.from(String(value || ''), 'base64url');
|
|
28
|
+
if (bytes.length !== 32) throw new Error(`${name} must be exactly 32 bytes.`);
|
|
29
|
+
return bytes;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildDeviceAuthExporterContext({ deviceId, mode }) {
|
|
33
|
+
if (!DEVICE_ID_PATTERN.test(String(deviceId || ''))) throw new Error('Invalid device_id.');
|
|
34
|
+
if (!(mode in DEVICE_AUTH_V2_MODE)) throw new Error('Invalid device authentication mode.');
|
|
35
|
+
return createHash('sha256').update(`hcu-mcp-device-auth-v2\n${deviceId}\n${mode}`, 'utf8').digest();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function buildDeviceAuthChallengeV2({ deviceId, mode, nonce, exporter, publicKeyPem, grant = null }) {
|
|
39
|
+
const deviceBytes = Buffer.from(String(deviceId || ''), 'utf8');
|
|
40
|
+
if (!DEVICE_ID_PATTERN.test(String(deviceId || '')) || deviceBytes.length > 0xffff) throw new Error('Invalid device_id.');
|
|
41
|
+
if (!(mode in DEVICE_AUTH_V2_MODE)) throw new Error('Invalid device authentication mode.');
|
|
42
|
+
const publicKey = createPublicKey(publicKeyPem);
|
|
43
|
+
if (publicKey.asymmetricKeyType !== 'ed25519') throw new Error('Device public key must be Ed25519.');
|
|
44
|
+
const publicKeyDer = publicKey.export({ type: 'spki', format: 'der' });
|
|
45
|
+
const grantDigest = mode === 'reconnect'
|
|
46
|
+
? Buffer.alloc(32)
|
|
47
|
+
: createHash('sha256').update(String(grant || ''), 'utf8').digest();
|
|
48
|
+
const deviceLength = Buffer.allocUnsafe(2);
|
|
49
|
+
deviceLength.writeUInt16BE(deviceBytes.length, 0);
|
|
50
|
+
return Buffer.concat([
|
|
51
|
+
Buffer.from('hcu-mcp-device-auth-v2\0', 'ascii'),
|
|
52
|
+
Buffer.from([2, DEVICE_AUTH_V2_MODE[mode]]),
|
|
53
|
+
deviceLength,
|
|
54
|
+
deviceBytes,
|
|
55
|
+
fixed32(nonce, 'gateway nonce'),
|
|
56
|
+
fixed32(exporter, 'TLS exporter'),
|
|
57
|
+
createHash('sha256').update(publicKeyDer).digest(),
|
|
58
|
+
grantDigest
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function bearerToken(request) {
|
|
63
|
+
const value = String(request.headers.authorization || '');
|
|
64
|
+
return value.startsWith('Bearer ') ? value.slice('Bearer '.length) : '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function tokenMatches(actual, expected) {
|
|
68
|
+
if (!actual || !expected) return false;
|
|
69
|
+
const left = Buffer.from(actual, 'utf8');
|
|
70
|
+
const right = Buffer.from(expected, 'utf8');
|
|
71
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function rejectUpgrade(socket, statusCode, message) {
|
|
75
|
+
if (!socket.writable) return socket.destroy();
|
|
76
|
+
const body = `${message}\n`;
|
|
77
|
+
socket.write(`HTTP/1.1 ${statusCode} ${message}\r\nConnection: close\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
|
|
78
|
+
socket.destroy();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function validateMessage(message, expectedVersion) {
|
|
82
|
+
if (!message || typeof message !== 'object') throw new Error('Device message must be an object.');
|
|
83
|
+
if (message.protocol_version !== expectedVersion) throw new Error('Unsupported device protocol version.');
|
|
84
|
+
if (typeof message.type !== 'string' || !message.type) throw new Error('Device message type is required.');
|
|
85
|
+
return message;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseMessage(raw, expectedVersion = DEVICE_PROTOCOL_VERSION) {
|
|
89
|
+
const text = Buffer.isBuffer(raw) ? raw.toString('utf8') : String(raw);
|
|
90
|
+
if (Buffer.byteLength(text, 'utf8') > MAX_MESSAGE_BYTES) throw new Error('Device message exceeds maximum size.');
|
|
91
|
+
return validateMessage(JSON.parse(text), expectedVersion);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeDeviceId(value) {
|
|
95
|
+
const deviceId = String(value || '').trim();
|
|
96
|
+
if (!DEVICE_ID_PATTERN.test(deviceId)) throw new Error('Invalid device_id.');
|
|
97
|
+
return deviceId;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizeCapabilities(value) {
|
|
101
|
+
if (!Array.isArray(value)) return [];
|
|
102
|
+
return [...new Set(value.map(item => String(item).trim()).filter(Boolean))].sort();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function machineMetadata(payload = {}) {
|
|
106
|
+
const pathStyle = String(payload.path_style || '').trim();
|
|
107
|
+
return {
|
|
108
|
+
hostname: String(payload.hostname || '').trim().slice(0, 128) || null,
|
|
109
|
+
platform: String(payload.platform || '').trim().slice(0, 32) || null,
|
|
110
|
+
arch: String(payload.arch || '').trim().slice(0, 32) || null,
|
|
111
|
+
pathStyle: ['windows', 'posix'].includes(pathStyle) ? pathStyle : null
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function runtimeMetadata(payload = {}) {
|
|
116
|
+
const hasReady = Object.prototype.hasOwnProperty.call(payload, 'runtime_ready');
|
|
117
|
+
const runtimeReady = hasReady
|
|
118
|
+
? payload.runtime_ready === true
|
|
119
|
+
: null;
|
|
120
|
+
const runtimeReason = runtimeReady === false
|
|
121
|
+
? (String(payload.runtime_reason || 'LOCAL_EXECUTION_ENGINE_UNAVAILABLE').trim().slice(0, 96) || 'LOCAL_EXECUTION_ENGINE_UNAVAILABLE')
|
|
122
|
+
: null;
|
|
123
|
+
const executionRuntimeGeneration = String(payload.execution_runtime_generation || '').trim().slice(0, 128) || null;
|
|
124
|
+
return { runtimeReady, runtimeReason, executionRuntimeGeneration };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function effectiveRuntimeGeneration(device) {
|
|
128
|
+
return device.executionRuntimeGeneration || `legacy:${device.connectionEpoch}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function publicDevice(device, { stored = null, usage = null, schema = null } = {}) {
|
|
132
|
+
return {
|
|
133
|
+
deviceId: device.deviceId,
|
|
134
|
+
deviceName: stored?.deviceName || device.deviceId,
|
|
135
|
+
hostname: device.hostname || stored?.hostname || null,
|
|
136
|
+
platform: device.platform || stored?.platform || null,
|
|
137
|
+
arch: device.arch || stored?.arch || null,
|
|
138
|
+
pathStyle: device.pathStyle || stored?.pathStyle || null,
|
|
139
|
+
account: {
|
|
140
|
+
connected: Boolean(stored?.ownerAccountId),
|
|
141
|
+
account_id: stored?.ownerAccountId || null,
|
|
142
|
+
label: stored?.accountLabel || null
|
|
143
|
+
},
|
|
144
|
+
online: Boolean(device.online),
|
|
145
|
+
revoked: Boolean(stored?.revokedAt || device.revoked),
|
|
146
|
+
connectionEpoch: device.connectionEpoch || 0,
|
|
147
|
+
runtimeReady: device.runtimeReady ?? null,
|
|
148
|
+
runtimeReason: device.runtimeReason || null,
|
|
149
|
+
executionRuntimeGeneration: device.executionRuntimeGeneration || null,
|
|
150
|
+
agentVersion: device.agentVersion || stored?.agentVersion || 'unknown',
|
|
151
|
+
packageVersion: device.packageVersion || stored?.packageVersion || null,
|
|
152
|
+
capabilities: [...(device.capabilities || [])],
|
|
153
|
+
connectedAt: device.connectedAt || null,
|
|
154
|
+
lastSeenAt: device.lastSeenAt || null,
|
|
155
|
+
usage,
|
|
156
|
+
schema
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function authFailure(ws, message = 'authentication failed') {
|
|
161
|
+
if (ws.readyState === WebSocket.OPEN) ws.close(4003, message.slice(0, 120));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function deviceBrokerError(message, code) {
|
|
165
|
+
const error = new Error(message);
|
|
166
|
+
error.code = code;
|
|
167
|
+
return error;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function createDeviceBroker(options = {}) {
|
|
171
|
+
const enrollmentToken = String(options.enrollmentToken || '').trim();
|
|
172
|
+
const deviceStore = options.deviceStore || null;
|
|
173
|
+
const pairingStore = options.pairingStore || null;
|
|
174
|
+
const usageStore = options.usageStore || null;
|
|
175
|
+
const auditRecorder = options.auditRecorder || null;
|
|
176
|
+
const onDeviceForgotten = typeof options.onDeviceForgotten === 'function' ? options.onDeviceForgotten : null;
|
|
177
|
+
const innerTls = options.innerTls || null;
|
|
178
|
+
const durableAuth = Boolean(deviceStore);
|
|
179
|
+
const requireAccountOwnership = options.requireAccountOwnership !== false;
|
|
180
|
+
let schemaSnapshot = normalizeSchemaSnapshot(options.schemaSnapshot);
|
|
181
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
182
|
+
const helloTimeoutMs = options.helloTimeoutMs ?? DEFAULT_HELLO_TIMEOUT_MS;
|
|
183
|
+
const updateTimeoutMs = options.updateTimeoutMs ?? DEFAULT_UPDATE_TIMEOUT_MS;
|
|
184
|
+
const devices = new Map();
|
|
185
|
+
const pending = new Map();
|
|
186
|
+
const pendingByWireRequestId = new Map();
|
|
187
|
+
const updateStates = new Map();
|
|
188
|
+
const updateTimers = new Map();
|
|
189
|
+
const activityStates = new Map();
|
|
190
|
+
const sockets = new Set();
|
|
191
|
+
// Internal adapters are registered by object identity, never by a wire-provided flag.
|
|
192
|
+
const localEndpoints = new WeakSet();
|
|
193
|
+
const wss = new WebSocketServer({ noServer: true, maxPayload: MAX_MESSAGE_BYTES });
|
|
194
|
+
let attachedServer = null;
|
|
195
|
+
let upgradeHandler = null;
|
|
196
|
+
|
|
197
|
+
function normalizeSchemaSnapshot(value = {}) {
|
|
198
|
+
const toolCount = Number.isInteger(Number(value.toolCount)) ? Number(value.toolCount) : 0;
|
|
199
|
+
const toolSchemaBytes = Number.isInteger(Number(value.toolSchemaBytes)) ? Number(value.toolSchemaBytes) : 0;
|
|
200
|
+
const toolSchemaTokenEstimate = Number.isInteger(Number(value.toolSchemaTokenEstimate))
|
|
201
|
+
? Number(value.toolSchemaTokenEstimate)
|
|
202
|
+
: Math.ceil(toolSchemaBytes / 4);
|
|
203
|
+
return {
|
|
204
|
+
toolCount,
|
|
205
|
+
toolSchemaBytes,
|
|
206
|
+
toolSchemaTokenEstimate,
|
|
207
|
+
tokenEstimateMethod: String(value.tokenEstimateMethod || 'utf8_bytes_div_4_estimate').slice(0, 64),
|
|
208
|
+
tokenUsageKind: 'schema_estimate_not_billing'
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function statusForDevice(device) {
|
|
213
|
+
const stored = durableAuth ? deviceStore.get(device.deviceId) : null;
|
|
214
|
+
const usage = usageStore ? usageStore.get(device.deviceId) : null;
|
|
215
|
+
const status = publicDevice(device, { stored, usage, schema: schemaSnapshot });
|
|
216
|
+
return { ...status, update: updateStates.get(device.deviceId) || null };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function clearUpdateTimer(deviceId) {
|
|
220
|
+
const timer = updateTimers.get(deviceId);
|
|
221
|
+
if (timer) clearTimeout(timer);
|
|
222
|
+
updateTimers.delete(deviceId);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function armUpdateTimer(deviceId, requestId) {
|
|
226
|
+
clearUpdateTimer(deviceId);
|
|
227
|
+
const delay = Number(updateTimeoutMs) > 0 ? Number(updateTimeoutMs) : DEFAULT_UPDATE_TIMEOUT_MS;
|
|
228
|
+
const timer = setTimeout(() => {
|
|
229
|
+
updateTimers.delete(deviceId);
|
|
230
|
+
const current = updateStates.get(deviceId);
|
|
231
|
+
if (!current || current.requestId !== requestId || !['requested', 'accepted', 'installed'].includes(current.state)) return;
|
|
232
|
+
updateStates.set(deviceId, {
|
|
233
|
+
...current,
|
|
234
|
+
state: 'failed',
|
|
235
|
+
updatedAt: Date.now(),
|
|
236
|
+
error: {
|
|
237
|
+
code: 'DEVICE_UPDATE_TIMEOUT',
|
|
238
|
+
message: 'Device update stopped reporting progress before completion; retry only after the device is online.'
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}, delay);
|
|
242
|
+
timer.unref?.();
|
|
243
|
+
updateTimers.set(deviceId, timer);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function recordDeviceStatus(device, status) {
|
|
247
|
+
if (!usageStore || !device?.deviceId) return;
|
|
248
|
+
const stored = durableAuth ? deviceStore.get(device.deviceId) : null;
|
|
249
|
+
usageStore.recordDeviceStatus(device.deviceId, {
|
|
250
|
+
accountId: stored?.ownerAccountId || null,
|
|
251
|
+
status,
|
|
252
|
+
connectionEpoch: device.connectionEpoch || 0,
|
|
253
|
+
agentVersion: device.agentVersion || null
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function noteActivitySent(deviceId, tool) {
|
|
258
|
+
const previous = activityStates.get(deviceId) || {};
|
|
259
|
+
activityStates.set(deviceId, {
|
|
260
|
+
...previous,
|
|
261
|
+
lastTool: String(tool || previous.lastTool || ''),
|
|
262
|
+
lastSentAt: Date.now()
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function noteActivityFinished(deviceId, tool, outcome = 'success') {
|
|
267
|
+
const previous = activityStates.get(deviceId) || {};
|
|
268
|
+
activityStates.set(deviceId, {
|
|
269
|
+
...previous,
|
|
270
|
+
lastTool: String(tool || previous.lastTool || ''),
|
|
271
|
+
lastReceivedAt: Date.now(),
|
|
272
|
+
lastOutcome: String(outcome || 'success')
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function getActivitySnapshot({ accountId = null } = {}) {
|
|
277
|
+
const owner = String(accountId || '').trim();
|
|
278
|
+
if (durableAuth && requireAccountOwnership && !owner) return [];
|
|
279
|
+
const pendingByDevice = new Map();
|
|
280
|
+
for (const entry of pending.values()) {
|
|
281
|
+
const current = pendingByDevice.get(entry.deviceId) || { count: 0, tool: null, startedAt: 0 };
|
|
282
|
+
current.count += 1;
|
|
283
|
+
if (Number(entry.startedAt || 0) >= current.startedAt) {
|
|
284
|
+
current.startedAt = Number(entry.startedAt || 0);
|
|
285
|
+
current.tool = entry.tool || current.tool;
|
|
286
|
+
}
|
|
287
|
+
pendingByDevice.set(entry.deviceId, current);
|
|
288
|
+
}
|
|
289
|
+
return [...devices.values()].flatMap(device => {
|
|
290
|
+
const stored = durableAuth ? deviceStore.get(device.deviceId) : null;
|
|
291
|
+
if (durableAuth && !stored) return [];
|
|
292
|
+
if (owner && stored?.ownerAccountId !== owner) return [];
|
|
293
|
+
const activity = activityStates.get(device.deviceId) || {};
|
|
294
|
+
const active = pendingByDevice.get(device.deviceId) || { count: 0, tool: null };
|
|
295
|
+
return [{
|
|
296
|
+
deviceId: device.deviceId,
|
|
297
|
+
inFlight: active.count,
|
|
298
|
+
tool: active.tool || activity.lastTool || null,
|
|
299
|
+
lastSentAt: activity.lastSentAt || null,
|
|
300
|
+
lastReceivedAt: activity.lastReceivedAt || null,
|
|
301
|
+
lastOutcome: activity.lastOutcome || null
|
|
302
|
+
}];
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function statusPayload(device) {
|
|
307
|
+
const status = statusForDevice(device);
|
|
308
|
+
return {
|
|
309
|
+
accepted: true,
|
|
310
|
+
account: status.account,
|
|
311
|
+
device: {
|
|
312
|
+
id: status.deviceId,
|
|
313
|
+
name: status.deviceName,
|
|
314
|
+
online: status.online,
|
|
315
|
+
connectionEpoch: status.connectionEpoch,
|
|
316
|
+
runtimeReady: status.runtimeReady,
|
|
317
|
+
runtimeReason: status.runtimeReason,
|
|
318
|
+
executionRuntimeGeneration: status.executionRuntimeGeneration,
|
|
319
|
+
connectedAt: status.connectedAt,
|
|
320
|
+
lastSeenAt: status.lastSeenAt
|
|
321
|
+
},
|
|
322
|
+
usage: status.usage,
|
|
323
|
+
schema: status.schema
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function socketProtocolVersion(ws) {
|
|
328
|
+
return ws?.deviceProtocolVersion === DEVICE_PROTOCOL_VERSION_V2
|
|
329
|
+
? DEVICE_PROTOCOL_VERSION_V2
|
|
330
|
+
: DEVICE_PROTOCOL_VERSION;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function sendDeviceMessage(ws, message, callback = undefined) {
|
|
334
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
335
|
+
callback?.(new Error('Device WebSocket is not open.'));
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
if (localEndpoints.has(ws)) {
|
|
339
|
+
ws.deliver(message, callback);
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
const protocolVersion = socketProtocolVersion(ws);
|
|
343
|
+
const payload = { ...message, protocol_version: protocolVersion };
|
|
344
|
+
if (protocolVersion === DEVICE_PROTOCOL_VERSION_V2) {
|
|
345
|
+
if (!ws.innerTlsSocket || ws.innerTlsSocket.destroyed) {
|
|
346
|
+
callback?.(new Error('Device inner TLS socket is not open.'));
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
const frame = encodeJsonFrame(payload, MAX_MESSAGE_BYTES);
|
|
350
|
+
ws.innerTlsSocket.write(frame, callback);
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
ws.send(JSON.stringify(payload), callback);
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function sendStatusSnapshot(ws, device) {
|
|
358
|
+
if (!ws || ws.readyState !== WebSocket.OPEN || !device) return;
|
|
359
|
+
sendDeviceMessage(ws, {
|
|
360
|
+
type: 'status_snapshot',
|
|
361
|
+
device_id: device.deviceId,
|
|
362
|
+
connection_epoch: device.connectionEpoch,
|
|
363
|
+
timestamp: Date.now(),
|
|
364
|
+
payload: statusPayload(device)
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (durableAuth) {
|
|
369
|
+
for (const stored of deviceStore.list()) {
|
|
370
|
+
if (stored.revokedAt) {
|
|
371
|
+
deviceStore.revoke(stored.deviceId);
|
|
372
|
+
auditRecorder?.recordEvent?.({
|
|
373
|
+
event: 'legacy_revoked_device_purged',
|
|
374
|
+
callerCategory: 'migration',
|
|
375
|
+
deviceId: stored.deviceId,
|
|
376
|
+
details: { deviceName: stored.deviceName || stored.deviceId }
|
|
377
|
+
});
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
devices.set(stored.deviceId, {
|
|
381
|
+
deviceId: stored.deviceId,
|
|
382
|
+
socket: null,
|
|
383
|
+
online: false,
|
|
384
|
+
revoked: false,
|
|
385
|
+
connectionEpoch: 0,
|
|
386
|
+
agentVersion: stored.agentVersion || 'unknown',
|
|
387
|
+
packageVersion: stored.packageVersion || null,
|
|
388
|
+
hostname: stored.hostname || null,
|
|
389
|
+
platform: stored.platform || null,
|
|
390
|
+
arch: stored.arch || null,
|
|
391
|
+
pathStyle: stored.pathStyle || null,
|
|
392
|
+
capabilities: [],
|
|
393
|
+
runtimeReady: null,
|
|
394
|
+
runtimeReason: null,
|
|
395
|
+
executionRuntimeGeneration: null,
|
|
396
|
+
connectedAt: null,
|
|
397
|
+
lastSeenAt: null,
|
|
398
|
+
authenticatedPublicKeyPem: null,
|
|
399
|
+
authenticatedAuthorizationGeneration: null
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function rejectPendingForConnection(deviceId, connectionEpoch, reason, { code = 'DEVICE_OFFLINE', outcome = 'disconnected' } = {}) {
|
|
405
|
+
for (const [requestId, entry] of pending) {
|
|
406
|
+
if (entry.deviceId !== deviceId || entry.connectionEpoch !== connectionEpoch) continue;
|
|
407
|
+
clearTimeout(entry.timer);
|
|
408
|
+
if (pending.get(requestId) !== entry) continue;
|
|
409
|
+
pending.delete(requestId);
|
|
410
|
+
if (pendingByWireRequestId.get(entry.wireRequestId) === entry) pendingByWireRequestId.delete(entry.wireRequestId);
|
|
411
|
+
noteActivityFinished(entry.deviceId, entry.tool, outcome);
|
|
412
|
+
entry.reject(deviceBrokerError(reason, code));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function registerConnection(ws, {
|
|
417
|
+
deviceId,
|
|
418
|
+
agentVersion,
|
|
419
|
+
packageVersion = null,
|
|
420
|
+
capabilities,
|
|
421
|
+
hostname = null,
|
|
422
|
+
platform = null,
|
|
423
|
+
arch = null,
|
|
424
|
+
pathStyle = null,
|
|
425
|
+
runtimeReady = null,
|
|
426
|
+
runtimeReason = null,
|
|
427
|
+
executionRuntimeGeneration = null,
|
|
428
|
+
publicKeyPem = null,
|
|
429
|
+
authorizationGeneration = null
|
|
430
|
+
}) {
|
|
431
|
+
const previous = devices.get(deviceId);
|
|
432
|
+
const connectionEpoch = (previous?.connectionEpoch || 0) + 1;
|
|
433
|
+
const current = {
|
|
434
|
+
deviceId,
|
|
435
|
+
socket: ws,
|
|
436
|
+
online: true,
|
|
437
|
+
revoked: false,
|
|
438
|
+
connectionEpoch,
|
|
439
|
+
agentVersion: String(agentVersion || 'unknown'),
|
|
440
|
+
packageVersion: String(packageVersion || '').trim().slice(0, 32) || null,
|
|
441
|
+
hostname: String(hostname || '').trim().slice(0, 128) || null,
|
|
442
|
+
platform: String(platform || '').trim().slice(0, 32) || null,
|
|
443
|
+
arch: String(arch || '').trim().slice(0, 32) || null,
|
|
444
|
+
pathStyle: ['windows', 'posix'].includes(String(pathStyle || '').trim()) ? String(pathStyle).trim() : null,
|
|
445
|
+
capabilities: normalizeCapabilities(capabilities),
|
|
446
|
+
runtimeReady: typeof runtimeReady === 'boolean' ? runtimeReady : null,
|
|
447
|
+
runtimeReason: runtimeReady === false ? (String(runtimeReason || 'LOCAL_EXECUTION_ENGINE_UNAVAILABLE').slice(0, 96) || 'LOCAL_EXECUTION_ENGINE_UNAVAILABLE') : null,
|
|
448
|
+
executionRuntimeGeneration: String(executionRuntimeGeneration || '').trim().slice(0, 128) || null,
|
|
449
|
+
connectedAt: Date.now(),
|
|
450
|
+
lastSeenAt: Date.now(),
|
|
451
|
+
authenticatedPublicKeyPem: durableAuth ? String(publicKeyPem || '') : null,
|
|
452
|
+
authenticatedAuthorizationGeneration: durableAuth ? Number(authorizationGeneration) : null
|
|
453
|
+
};
|
|
454
|
+
devices.set(deviceId, current);
|
|
455
|
+
const update = updateStates.get(deviceId);
|
|
456
|
+
if (update?.targetVersion && current.packageVersion === update.targetVersion) {
|
|
457
|
+
clearUpdateTimer(deviceId);
|
|
458
|
+
updateStates.set(deviceId, { ...update, state: 'complete', completedAt: Date.now(), error: null });
|
|
459
|
+
}
|
|
460
|
+
ws.deviceId = deviceId;
|
|
461
|
+
ws.connectionEpoch = connectionEpoch;
|
|
462
|
+
ws.authenticated = true;
|
|
463
|
+
if (previous?.socket && previous.socket !== ws) {
|
|
464
|
+
rejectPendingForConnection(
|
|
465
|
+
deviceId,
|
|
466
|
+
previous.connectionEpoch,
|
|
467
|
+
'Device connection was replaced before the request result was known; the request was not replayed.'
|
|
468
|
+
);
|
|
469
|
+
if (previous.socket.readyState === WebSocket.OPEN) previous.socket.close(4001, 'replaced by newer connection');
|
|
470
|
+
}
|
|
471
|
+
return current;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function applyRuntimeState(device, payload = {}) {
|
|
475
|
+
const hasReady = Object.prototype.hasOwnProperty.call(payload, 'runtime_ready');
|
|
476
|
+
const hasReason = Object.prototype.hasOwnProperty.call(payload, 'runtime_reason');
|
|
477
|
+
const hasGeneration = Object.prototype.hasOwnProperty.call(payload, 'execution_runtime_generation');
|
|
478
|
+
if (!hasReady && !hasReason && !hasGeneration) return;
|
|
479
|
+
|
|
480
|
+
const beforeGeneration = effectiveRuntimeGeneration(device);
|
|
481
|
+
const metadata = runtimeMetadata(payload);
|
|
482
|
+
if (hasReady) device.runtimeReady = metadata.runtimeReady;
|
|
483
|
+
if (hasReady || hasReason) {
|
|
484
|
+
device.runtimeReason = device.runtimeReady === false
|
|
485
|
+
? metadata.runtimeReason
|
|
486
|
+
: null;
|
|
487
|
+
}
|
|
488
|
+
if (hasGeneration) device.executionRuntimeGeneration = metadata.executionRuntimeGeneration;
|
|
489
|
+
const afterGeneration = effectiveRuntimeGeneration(device);
|
|
490
|
+
|
|
491
|
+
if (device.runtimeReady === false) {
|
|
492
|
+
rejectPendingForConnection(
|
|
493
|
+
device.deviceId,
|
|
494
|
+
device.connectionEpoch,
|
|
495
|
+
'Device execution runtime became unavailable before the request result was known; the request was not replayed.',
|
|
496
|
+
{ code: 'DEVICE_NOT_READY', outcome: 'not_ready' }
|
|
497
|
+
);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (beforeGeneration !== afterGeneration) {
|
|
501
|
+
rejectPendingForConnection(
|
|
502
|
+
device.deviceId,
|
|
503
|
+
device.connectionEpoch,
|
|
504
|
+
'Device execution runtime changed before the request result was known; the request was not replayed.',
|
|
505
|
+
{ code: 'DEVICE_RUNTIME_CHANGED', outcome: 'runtime_changed' }
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function invalidateAuthorization(device, { revoked, reason, closeCode }) {
|
|
511
|
+
const socket = device.socket;
|
|
512
|
+
const connectionEpoch = device.connectionEpoch;
|
|
513
|
+
rejectPendingForConnection(
|
|
514
|
+
device.deviceId,
|
|
515
|
+
connectionEpoch,
|
|
516
|
+
`${reason} before the request result was known; the request was not replayed.`
|
|
517
|
+
);
|
|
518
|
+
if (revoked) {
|
|
519
|
+
clearUpdateTimer(device.deviceId);
|
|
520
|
+
updateStates.delete(device.deviceId);
|
|
521
|
+
activityStates.delete(device.deviceId);
|
|
522
|
+
devices.delete(device.deviceId);
|
|
523
|
+
try { onDeviceForgotten?.({ deviceId: device.deviceId, accountId: null }); }
|
|
524
|
+
catch (error) { console.error(`[device-broker] device cleanup callback failed: ${error.message}`); }
|
|
525
|
+
auditRecorder?.recordEvent?.({
|
|
526
|
+
event: 'device_forget_observed',
|
|
527
|
+
callerCategory: 'broker',
|
|
528
|
+
deviceId: device.deviceId,
|
|
529
|
+
details: { reason: String(reason || 'authorization removed').slice(0, 160) }
|
|
530
|
+
});
|
|
531
|
+
if (socket?.readyState === WebSocket.OPEN) socket.close(closeCode, reason.slice(0, 120));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
device.revoked = false;
|
|
535
|
+
device.online = false;
|
|
536
|
+
device.socket = null;
|
|
537
|
+
device.lastSeenAt = Date.now();
|
|
538
|
+
device.authenticatedPublicKeyPem = null;
|
|
539
|
+
device.authenticatedAuthorizationGeneration = null;
|
|
540
|
+
recordDeviceStatus(device, 'offline');
|
|
541
|
+
if (socket?.readyState === WebSocket.OPEN) socket.close(closeCode, reason.slice(0, 120));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function refreshAuthorization(device) {
|
|
545
|
+
if (!durableAuth || !device?.online || !device.socket) return true;
|
|
546
|
+
const stored = deviceStore.get(device.deviceId);
|
|
547
|
+
if (!stored || stored.revokedAt) {
|
|
548
|
+
invalidateAuthorization(device, { revoked: true, reason: 'Device was revoked', closeCode: 4004 });
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
if (!device.authenticatedPublicKeyPem || stored.publicKeyPem !== device.authenticatedPublicKeyPem || stored.authorizationGeneration !== device.authenticatedAuthorizationGeneration) {
|
|
552
|
+
invalidateAuthorization(device, { revoked: false, reason: 'Device authorization changed', closeCode: 4005 });
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function sendAuthChallenge(ws, authState) {
|
|
559
|
+
const nonce = randomBytes(32).toString('base64url');
|
|
560
|
+
const protocolVersion = socketProtocolVersion(ws);
|
|
561
|
+
let exporter = null;
|
|
562
|
+
if (protocolVersion === DEVICE_PROTOCOL_VERSION_V2) {
|
|
563
|
+
exporter = ws.innerTlsSocket.exportKeyingMaterial(
|
|
564
|
+
32,
|
|
565
|
+
DEVICE_AUTH_EXPORTER_LABEL,
|
|
566
|
+
buildDeviceAuthExporterContext({ deviceId: authState.deviceId, mode: authState.mode })
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
ws.authState = { ...authState, nonce, exporter };
|
|
570
|
+
sendDeviceMessage(ws, {
|
|
571
|
+
type: 'auth_challenge',
|
|
572
|
+
device_id: authState.deviceId,
|
|
573
|
+
timestamp: Date.now(),
|
|
574
|
+
payload: {
|
|
575
|
+
nonce,
|
|
576
|
+
...(protocolVersion === DEVICE_PROTOCOL_VERSION_V2 ? { mode: authState.mode } : {})
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function handleAuthHello(ws, message) {
|
|
582
|
+
const deviceId = normalizeDeviceId(message.device_id);
|
|
583
|
+
const v2 = socketProtocolVersion(ws) === DEVICE_PROTOCOL_VERSION_V2;
|
|
584
|
+
const innerCredential = v2 ? String(message.payload?.enrollment_grant || '').trim() : '';
|
|
585
|
+
if (message.type === 'enroll_hello') {
|
|
586
|
+
const pairingCredential = v2 ? innerCredential : String(ws.enrollmentCredential || '').trim();
|
|
587
|
+
const legacyEnrollmentAuthorized = v2
|
|
588
|
+
? tokenMatches(pairingCredential, enrollmentToken)
|
|
589
|
+
: Boolean(ws.enrollmentAuthorized);
|
|
590
|
+
if (!legacyEnrollmentAuthorized && !(pairingStore && pairingCredential)) {
|
|
591
|
+
return authFailure(ws, 'enrollment authorization required');
|
|
592
|
+
}
|
|
593
|
+
const existing = deviceStore.get(deviceId);
|
|
594
|
+
if (existing) return authFailure(ws, existing.revokedAt ? 'device revoked' : 'device already enrolled');
|
|
595
|
+
const publicKeyPem = String(message.payload?.public_key_pem || '');
|
|
596
|
+
if (!publicKeyPem) return authFailure(ws, 'public key required');
|
|
597
|
+
sendAuthChallenge(ws, {
|
|
598
|
+
mode: 'enroll',
|
|
599
|
+
deviceId,
|
|
600
|
+
publicKeyPem,
|
|
601
|
+
agentVersion: message.payload?.agent_version,
|
|
602
|
+
packageVersion: message.payload?.package_version,
|
|
603
|
+
capabilities: message.payload?.capabilities,
|
|
604
|
+
...machineMetadata(message.payload),
|
|
605
|
+
...runtimeMetadata(message.payload),
|
|
606
|
+
legacyEnrollmentAuthorized,
|
|
607
|
+
enrollmentGrant: pairingCredential
|
|
608
|
+
});
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const stored = deviceStore.get(deviceId);
|
|
612
|
+
// In-process proof is not a network protocol downgrade; the persisted wire floor remains unchanged.
|
|
613
|
+
if (!v2 && stored?.minProtocol >= 2 && !localEndpoints.has(ws)) return authFailure(ws, 'device requires protocol v2');
|
|
614
|
+
if (message.type === 'pair_hello') {
|
|
615
|
+
const pairingCredential = v2 ? innerCredential : String(ws.enrollmentCredential || '').trim();
|
|
616
|
+
if (!pairingStore || !pairingCredential) return authFailure(ws, 'pairing grant required');
|
|
617
|
+
if (stored?.revokedAt) return authFailure(ws, 'revoked device');
|
|
618
|
+
const publicKeyPem = stored?.publicKeyPem || String(message.payload?.public_key_pem || '');
|
|
619
|
+
if (!publicKeyPem) return authFailure(ws, 'public key required for device migration');
|
|
620
|
+
sendAuthChallenge(ws, {
|
|
621
|
+
mode: 'pair',
|
|
622
|
+
deviceId,
|
|
623
|
+
publicKeyPem,
|
|
624
|
+
agentVersion: message.payload?.agent_version,
|
|
625
|
+
packageVersion: message.payload?.package_version,
|
|
626
|
+
capabilities: message.payload?.capabilities,
|
|
627
|
+
...machineMetadata(message.payload),
|
|
628
|
+
...runtimeMetadata(message.payload),
|
|
629
|
+
authorizationGeneration: stored?.authorizationGeneration ?? null,
|
|
630
|
+
wasEnrolled: Boolean(stored),
|
|
631
|
+
enrollmentGrant: pairingCredential
|
|
632
|
+
});
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (message.type !== 'auth_hello') return authFailure(ws, 'first message must be enroll_hello, pair_hello, or auth_hello');
|
|
636
|
+
if (!stored || stored.revokedAt) return authFailure(ws, 'unknown or revoked device');
|
|
637
|
+
sendAuthChallenge(ws, {
|
|
638
|
+
mode: 'reconnect',
|
|
639
|
+
deviceId,
|
|
640
|
+
publicKeyPem: stored.publicKeyPem,
|
|
641
|
+
agentVersion: message.payload?.agent_version,
|
|
642
|
+
packageVersion: message.payload?.package_version,
|
|
643
|
+
capabilities: message.payload?.capabilities,
|
|
644
|
+
...machineMetadata(message.payload),
|
|
645
|
+
...runtimeMetadata(message.payload),
|
|
646
|
+
authorizationGeneration: stored.authorizationGeneration
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function handleAuthResponse(ws, message) {
|
|
651
|
+
const state = ws.authState;
|
|
652
|
+
if (!state || message.type !== 'auth_response' || normalizeDeviceId(message.device_id) !== state.deviceId) {
|
|
653
|
+
return authFailure(ws, 'invalid authentication response');
|
|
654
|
+
}
|
|
655
|
+
let signature;
|
|
656
|
+
try { signature = Buffer.from(String(message.payload?.signature || ''), 'base64'); } catch { return authFailure(ws); }
|
|
657
|
+
const challenge = socketProtocolVersion(ws) === DEVICE_PROTOCOL_VERSION_V2
|
|
658
|
+
? buildDeviceAuthChallengeV2({
|
|
659
|
+
deviceId: state.deviceId,
|
|
660
|
+
mode: state.mode,
|
|
661
|
+
nonce: state.nonce,
|
|
662
|
+
exporter: state.exporter,
|
|
663
|
+
publicKeyPem: state.publicKeyPem,
|
|
664
|
+
grant: state.enrollmentGrant
|
|
665
|
+
})
|
|
666
|
+
: buildDeviceAuthChallenge({ deviceId: state.deviceId, nonce: state.nonce });
|
|
667
|
+
const valid = signature.length > 0 && verify(null, challenge, state.publicKeyPem, signature);
|
|
668
|
+
if (!valid) return authFailure(ws, 'invalid signature');
|
|
669
|
+
|
|
670
|
+
let authorizedPublicKeyPem = state.publicKeyPem;
|
|
671
|
+
let authorizationGeneration;
|
|
672
|
+
if (state.mode === 'enroll') {
|
|
673
|
+
try {
|
|
674
|
+
let pairing = null;
|
|
675
|
+
if (!state.legacyEnrollmentAuthorized) {
|
|
676
|
+
pairing = pairingStore.consumeGrant({
|
|
677
|
+
enrollmentGrant: state.enrollmentGrant,
|
|
678
|
+
deviceId: state.deviceId,
|
|
679
|
+
publicKeyPem: state.publicKeyPem
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
const enrolled = deviceStore.enroll({
|
|
683
|
+
deviceId: state.deviceId,
|
|
684
|
+
publicKeyPem: state.publicKeyPem,
|
|
685
|
+
deviceName: pairing?.deviceName || state.deviceId,
|
|
686
|
+
ownerAccountId: pairing?.account?.account_id || null,
|
|
687
|
+
accountLabel: pairing?.account?.label || null,
|
|
688
|
+
hostname: state.hostname,
|
|
689
|
+
platform: state.platform,
|
|
690
|
+
arch: state.arch,
|
|
691
|
+
pathStyle: state.pathStyle,
|
|
692
|
+
agentVersion: state.agentVersion,
|
|
693
|
+
packageVersion: state.packageVersion
|
|
694
|
+
});
|
|
695
|
+
authorizedPublicKeyPem = enrolled.publicKeyPem;
|
|
696
|
+
authorizationGeneration = enrolled.authorizationGeneration;
|
|
697
|
+
} catch {
|
|
698
|
+
return authFailure(ws, 'enrollment failed');
|
|
699
|
+
}
|
|
700
|
+
} else if (state.mode === 'pair') {
|
|
701
|
+
try {
|
|
702
|
+
const pairing = pairingStore.consumeGrant({
|
|
703
|
+
enrollmentGrant: state.enrollmentGrant,
|
|
704
|
+
deviceId: state.deviceId,
|
|
705
|
+
publicKeyPem: state.publicKeyPem
|
|
706
|
+
});
|
|
707
|
+
const current = deviceStore.get(state.deviceId);
|
|
708
|
+
let updated;
|
|
709
|
+
if (!current) {
|
|
710
|
+
updated = deviceStore.enroll({
|
|
711
|
+
deviceId: state.deviceId,
|
|
712
|
+
publicKeyPem: state.publicKeyPem,
|
|
713
|
+
deviceName: pairing.deviceName,
|
|
714
|
+
ownerAccountId: pairing.account?.account_id || null,
|
|
715
|
+
accountLabel: pairing.account?.label || null,
|
|
716
|
+
hostname: state.hostname,
|
|
717
|
+
platform: state.platform,
|
|
718
|
+
arch: state.arch,
|
|
719
|
+
pathStyle: state.pathStyle,
|
|
720
|
+
agentVersion: state.agentVersion,
|
|
721
|
+
packageVersion: state.packageVersion
|
|
722
|
+
});
|
|
723
|
+
} else {
|
|
724
|
+
if (current.revokedAt || current.publicKeyPem !== state.publicKeyPem) throw new Error('device authorization changed');
|
|
725
|
+
if (state.wasEnrolled && current.authorizationGeneration !== state.authorizationGeneration) throw new Error('device authorization changed');
|
|
726
|
+
updated = deviceStore.updateMetadata({
|
|
727
|
+
deviceId: state.deviceId,
|
|
728
|
+
deviceName: pairing.deviceName,
|
|
729
|
+
hostname: state.hostname,
|
|
730
|
+
platform: state.platform,
|
|
731
|
+
arch: state.arch,
|
|
732
|
+
pathStyle: state.pathStyle,
|
|
733
|
+
agentVersion: state.agentVersion,
|
|
734
|
+
packageVersion: state.packageVersion
|
|
735
|
+
});
|
|
736
|
+
updated = deviceStore.assignOwner({
|
|
737
|
+
deviceId: state.deviceId,
|
|
738
|
+
ownerAccountId: pairing.account?.account_id || null,
|
|
739
|
+
accountLabel: pairing.account?.label || null
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
authorizedPublicKeyPem = updated.publicKeyPem;
|
|
743
|
+
authorizationGeneration = updated.authorizationGeneration;
|
|
744
|
+
} catch {
|
|
745
|
+
return authFailure(ws, 'account pairing failed');
|
|
746
|
+
}
|
|
747
|
+
} else {
|
|
748
|
+
authorizedPublicKeyPem = state.publicKeyPem;
|
|
749
|
+
authorizationGeneration = state.authorizationGeneration;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
try {
|
|
753
|
+
const current = deviceStore.withCurrentAuthorization(
|
|
754
|
+
{ deviceId: state.deviceId, publicKeyPem: authorizedPublicKeyPem, authorizationGeneration },
|
|
755
|
+
() => registerConnection(ws, { ...state, publicKeyPem: authorizedPublicKeyPem, authorizationGeneration })
|
|
756
|
+
);
|
|
757
|
+
if ((state.agentVersion !== undefined || state.packageVersion !== undefined) && typeof deviceStore.updateMetadata === 'function') {
|
|
758
|
+
try { deviceStore.updateMetadata({ deviceId: state.deviceId, agentVersion: state.agentVersion, packageVersion: state.packageVersion }); }
|
|
759
|
+
catch (error) { console.error(`[device-broker] device version persist failed: ${error.message}`); }
|
|
760
|
+
}
|
|
761
|
+
if (socketProtocolVersion(ws) === DEVICE_PROTOCOL_VERSION_V2 && typeof deviceStore.raiseProtocolFloor === 'function') {
|
|
762
|
+
deviceStore.raiseProtocolFloor(state.deviceId, DEVICE_PROTOCOL_VERSION_V2);
|
|
763
|
+
}
|
|
764
|
+
ws.authState = null;
|
|
765
|
+
if (usageStore) {
|
|
766
|
+
const previousUsage = usageStore.get(current.deviceId);
|
|
767
|
+
usageStore.recordConnection(current.deviceId, { reconnect: previousUsage.connections > 0 });
|
|
768
|
+
recordDeviceStatus(current, 'online');
|
|
769
|
+
}
|
|
770
|
+
sendDeviceMessage(ws, {
|
|
771
|
+
type: 'auth_ok',
|
|
772
|
+
device_id: current.deviceId,
|
|
773
|
+
connection_epoch: current.connectionEpoch,
|
|
774
|
+
timestamp: Date.now(),
|
|
775
|
+
payload: statusPayload(current)
|
|
776
|
+
});
|
|
777
|
+
} catch { return authFailure(ws, 'device authorization changed'); }
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function registerLegacyHello(ws, message) {
|
|
781
|
+
const deviceId = normalizeDeviceId(message.device_id);
|
|
782
|
+
const current = registerConnection(ws, {
|
|
783
|
+
deviceId,
|
|
784
|
+
agentVersion: message.payload?.agent_version,
|
|
785
|
+
packageVersion: message.payload?.package_version,
|
|
786
|
+
capabilities: message.payload?.capabilities,
|
|
787
|
+
...machineMetadata(message.payload),
|
|
788
|
+
...runtimeMetadata(message.payload)
|
|
789
|
+
});
|
|
790
|
+
ws.send(JSON.stringify({
|
|
791
|
+
protocol_version: DEVICE_PROTOCOL_VERSION,
|
|
792
|
+
type: 'hello_ack',
|
|
793
|
+
device_id: deviceId,
|
|
794
|
+
connection_epoch: current.connectionEpoch,
|
|
795
|
+
timestamp: Date.now(),
|
|
796
|
+
payload: { accepted: true }
|
|
797
|
+
}));
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function handleRegisteredMessage(ws, message) {
|
|
801
|
+
const device = devices.get(ws.deviceId);
|
|
802
|
+
if (!device || device.socket !== ws || device.connectionEpoch !== ws.connectionEpoch) return;
|
|
803
|
+
if (!refreshAuthorization(device)) return;
|
|
804
|
+
if (message.device_id && message.device_id !== ws.deviceId) throw new Error('device_id does not match authenticated connection.');
|
|
805
|
+
if (message.connection_epoch !== undefined && message.connection_epoch !== ws.connectionEpoch) return;
|
|
806
|
+
device.lastSeenAt = Date.now();
|
|
807
|
+
if (message.type === 'account_logout') {
|
|
808
|
+
if (!durableAuth) return;
|
|
809
|
+
const updated = deviceStore.assignOwner({ deviceId: device.deviceId, ownerAccountId: null, accountLabel: null });
|
|
810
|
+
device.authenticatedAuthorizationGeneration = updated.authorizationGeneration;
|
|
811
|
+
usageStore?.touch(device.deviceId);
|
|
812
|
+
sendStatusSnapshot(ws, device);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (message.type === 'heartbeat') {
|
|
816
|
+
applyRuntimeState(device, message.payload || {});
|
|
817
|
+
usageStore?.touch(device.deviceId);
|
|
818
|
+
sendStatusSnapshot(ws, device);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (message.type === 'capability_sync') {
|
|
822
|
+
device.capabilities = normalizeCapabilities(message.payload?.capabilities);
|
|
823
|
+
device.agentVersion = String(message.payload?.agent_version || device.agentVersion);
|
|
824
|
+
device.packageVersion = String(message.payload?.package_version || device.packageVersion || '').trim().slice(0, 32) || null;
|
|
825
|
+
if (durableAuth && (message.payload?.agent_version !== undefined || message.payload?.package_version !== undefined)) {
|
|
826
|
+
deviceStore.updateMetadata({ deviceId: device.deviceId, agentVersion: device.agentVersion, packageVersion: device.packageVersion });
|
|
827
|
+
}
|
|
828
|
+
usageStore?.touch(device.deviceId);
|
|
829
|
+
sendStatusSnapshot(ws, device);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (message.type === 'device_update_status') {
|
|
833
|
+
const current = updateStates.get(device.deviceId);
|
|
834
|
+
const requestId = String(message.request_id || '').trim();
|
|
835
|
+
const targetVersion = String(message.payload?.target_version || '').trim();
|
|
836
|
+
if (!current || current.requestId !== requestId || current.targetVersion !== targetVersion) return;
|
|
837
|
+
const state = String(message.payload?.state || '').trim();
|
|
838
|
+
if (!['accepted', 'installed', 'failed'].includes(state)) return;
|
|
839
|
+
updateStates.set(device.deviceId, {
|
|
840
|
+
...current,
|
|
841
|
+
state,
|
|
842
|
+
updatedAt: Date.now(),
|
|
843
|
+
error: state === 'failed' ? {
|
|
844
|
+
code: String(message.payload?.code || 'DEVICE_UPDATE_FAILED').slice(0, 64),
|
|
845
|
+
message: String(message.payload?.message || 'Device update failed.').slice(0, 240)
|
|
846
|
+
} : null
|
|
847
|
+
});
|
|
848
|
+
if (state === 'failed') clearUpdateTimer(device.deviceId);
|
|
849
|
+
else armUpdateTimer(device.deviceId, requestId);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (message.type !== 'tool_result' && message.type !== 'tool_error') return;
|
|
853
|
+
const wireRequestId = String(message.request_id || '');
|
|
854
|
+
const responseBytes = Buffer.byteLength(JSON.stringify(message), 'utf8');
|
|
855
|
+
let usageOutcome = null;
|
|
856
|
+
const acceptResult = () => {
|
|
857
|
+
const entry = pendingByWireRequestId.get(wireRequestId);
|
|
858
|
+
if (!entry || entry.deviceId !== ws.deviceId || entry.connectionEpoch !== ws.connectionEpoch || pending.get(entry.requestId) !== entry) return;
|
|
859
|
+
clearTimeout(entry.timer);
|
|
860
|
+
pending.delete(entry.requestId);
|
|
861
|
+
if (pendingByWireRequestId.get(wireRequestId) === entry) pendingByWireRequestId.delete(wireRequestId);
|
|
862
|
+
noteActivityFinished(device.deviceId, entry.tool, message.type === 'tool_error' ? 'error' : 'success');
|
|
863
|
+
if (message.type === 'tool_error') {
|
|
864
|
+
const error = new Error(String(message.payload?.message || 'Remote device tool error.'));
|
|
865
|
+
error.code = String(message.payload?.code || 'REMOTE_DEVICE_ERROR').slice(0, 64);
|
|
866
|
+
usageOutcome = { ok: false, errorCode: error.code };
|
|
867
|
+
entry.reject(error);
|
|
868
|
+
} else {
|
|
869
|
+
usageOutcome = { ok: true };
|
|
870
|
+
entry.resolve(entry.includeDispatchContext
|
|
871
|
+
? { result: message.payload, dispatchContext: entry.dispatchContext }
|
|
872
|
+
: message.payload);
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
try {
|
|
876
|
+
if (!durableAuth) acceptResult();
|
|
877
|
+
else {
|
|
878
|
+
deviceStore.withCurrentAuthorization(
|
|
879
|
+
{ deviceId: device.deviceId, publicKeyPem: device.authenticatedPublicKeyPem, authorizationGeneration: device.authenticatedAuthorizationGeneration },
|
|
880
|
+
acceptResult
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
if (usageOutcome && usageStore) {
|
|
884
|
+
if (usageOutcome.ok) usageStore.recordToolSucceeded(device.deviceId, responseBytes);
|
|
885
|
+
else usageStore.recordToolFailed(device.deviceId, responseBytes, usageOutcome.errorCode);
|
|
886
|
+
sendStatusSnapshot(ws, device);
|
|
887
|
+
}
|
|
888
|
+
} catch {
|
|
889
|
+
invalidateAuthorization(device, { revoked: false, reason: 'Device authorization changed', closeCode: 4005 });
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
wss.on('connection', (ws, request) => {
|
|
894
|
+
sockets.add(ws);
|
|
895
|
+
ws.deviceProtocolVersion = ws.protocol === DEVICE_INNER_TLS_SUBPROTOCOL
|
|
896
|
+
? DEVICE_PROTOCOL_VERSION_V2
|
|
897
|
+
: DEVICE_PROTOCOL_VERSION;
|
|
898
|
+
const helloTimer = setTimeout(() => {
|
|
899
|
+
if (!ws.authenticated && ws.readyState === WebSocket.OPEN) ws.close(4000, 'authentication required');
|
|
900
|
+
}, helloTimeoutMs);
|
|
901
|
+
helloTimer.unref?.();
|
|
902
|
+
|
|
903
|
+
const handleLogicalMessage = message => {
|
|
904
|
+
try {
|
|
905
|
+
validateMessage(message, socketProtocolVersion(ws));
|
|
906
|
+
if (!ws.authenticated) {
|
|
907
|
+
if (durableAuth) {
|
|
908
|
+
if (ws.authState) handleAuthResponse(ws, message);
|
|
909
|
+
else handleAuthHello(ws, message);
|
|
910
|
+
if (ws.authenticated) clearTimeout(helloTimer);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
if (message.type !== 'hello') throw new Error('First device message must be hello.');
|
|
914
|
+
registerLegacyHello(ws, message);
|
|
915
|
+
clearTimeout(helloTimer);
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
handleRegisteredMessage(ws, message);
|
|
919
|
+
} catch (error) {
|
|
920
|
+
if (ws.readyState === WebSocket.OPEN) ws.close(durableAuth ? 4003 : 4002, String(error.message).slice(0, 120));
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
if (ws.deviceProtocolVersion === DEVICE_PROTOCOL_VERSION_V2) {
|
|
925
|
+
if (!innerTls) {
|
|
926
|
+
ws.close(1011, 'device inner TLS is unavailable');
|
|
927
|
+
} else {
|
|
928
|
+
const secureSocket = createServerInnerTls(ws, innerTls);
|
|
929
|
+
ws.innerTlsSocket = secureSocket;
|
|
930
|
+
const frameParser = createJsonFrameParser({
|
|
931
|
+
maxMessageBytes: MAX_MESSAGE_BYTES,
|
|
932
|
+
onMessage: handleLogicalMessage
|
|
933
|
+
});
|
|
934
|
+
secureSocket.on('data', chunk => {
|
|
935
|
+
try { frameParser.push(chunk); }
|
|
936
|
+
catch (error) {
|
|
937
|
+
if (ws.readyState === WebSocket.OPEN) ws.close(4003, String(error.message).slice(0, 120));
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
secureSocket.once('error', error => {
|
|
941
|
+
if (ws.readyState === WebSocket.OPEN) ws.close(4003, String(error.message).slice(0, 120));
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
} else {
|
|
945
|
+
ws.on('message', raw => {
|
|
946
|
+
try { handleLogicalMessage(parseMessage(raw)); }
|
|
947
|
+
catch (error) {
|
|
948
|
+
if (ws.readyState === WebSocket.OPEN) ws.close(durableAuth ? 4003 : 4002, String(error.message).slice(0, 120));
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
ws.on('close', () => {
|
|
954
|
+
sockets.delete(ws);
|
|
955
|
+
clearTimeout(helloTimer);
|
|
956
|
+
try { ws.innerTlsSocket?.destroy(); } catch {}
|
|
957
|
+
if (!ws.deviceId) return;
|
|
958
|
+
const current = devices.get(ws.deviceId);
|
|
959
|
+
if (!current || current.socket !== ws || current.connectionEpoch !== ws.connectionEpoch) return;
|
|
960
|
+
current.online = false;
|
|
961
|
+
current.socket = null;
|
|
962
|
+
current.lastSeenAt = Date.now();
|
|
963
|
+
recordDeviceStatus(current, 'offline');
|
|
964
|
+
rejectPendingForConnection(ws.deviceId, ws.connectionEpoch, 'Device disconnected before the request result was known; the request was not replayed.');
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Authenticate one embedded Device and dispatch directly into the shared Core.
|
|
970
|
+
* The normal proof/grant handlers, pending map, owner checks and generation
|
|
971
|
+
* checks are shared with remote Devices. No TCP/WebSocket or child runtime exists.
|
|
972
|
+
*/
|
|
973
|
+
async function attachLocalDevice({
|
|
974
|
+
deviceId, publicKeyPem, signChallenge, invoke, capabilities = [],
|
|
975
|
+
enrollmentGrant = null, ...metadata
|
|
976
|
+
}) {
|
|
977
|
+
if (!durableAuth) throw new Error('Embedded Device requires durable identity and account ownership.');
|
|
978
|
+
if (typeof signChallenge !== 'function' || typeof invoke !== 'function') {
|
|
979
|
+
throw new Error('Embedded Device requires proof signer and Core invocation adapter.');
|
|
980
|
+
}
|
|
981
|
+
const normalizedId = normalizeDeviceId(deviceId);
|
|
982
|
+
const suppliedKey = createPublicKey(publicKeyPem).export({ type: 'spki', format: 'pem' }).toString();
|
|
983
|
+
const stored = deviceStore.get(normalizedId);
|
|
984
|
+
if (stored && stored.publicKeyPem !== suppliedKey) {
|
|
985
|
+
throw deviceBrokerError('Embedded Device public key differs from enrolled identity.', 'DEVICE_IDENTITY_MISMATCH');
|
|
986
|
+
}
|
|
987
|
+
let authenticated = false;
|
|
988
|
+
let resolveAuth;
|
|
989
|
+
let rejectAuth;
|
|
990
|
+
const authenticatedPromise = new Promise((resolve, reject) => { resolveAuth = resolve; rejectAuth = reject; });
|
|
991
|
+
const endpoint = {
|
|
992
|
+
readyState: WebSocket.OPEN,
|
|
993
|
+
deviceProtocolVersion: DEVICE_PROTOCOL_VERSION,
|
|
994
|
+
enrollmentCredential: enrollmentGrant || '',
|
|
995
|
+
enrollmentAuthorized: false,
|
|
996
|
+
deliver(message, callback) {
|
|
997
|
+
if (endpoint.readyState !== WebSocket.OPEN) {
|
|
998
|
+
callback?.(deviceBrokerError('Embedded Device transport is closed.', 'DEVICE_OFFLINE'));
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (message.type === 'auth_challenge') {
|
|
1002
|
+
Promise.resolve().then(() => signChallenge(message.payload.nonce)).then(signature => {
|
|
1003
|
+
if (endpoint.readyState !== WebSocket.OPEN) return;
|
|
1004
|
+
handleAuthResponse(endpoint, {
|
|
1005
|
+
type: 'auth_response', device_id: normalizedId,
|
|
1006
|
+
payload: { signature }
|
|
1007
|
+
});
|
|
1008
|
+
}).catch(error => {
|
|
1009
|
+
rejectAuth(error);
|
|
1010
|
+
endpoint.close(4003, 'identity proof failed');
|
|
1011
|
+
});
|
|
1012
|
+
} else if (message.type === 'auth_ok') {
|
|
1013
|
+
authenticated = true;
|
|
1014
|
+
resolveAuth();
|
|
1015
|
+
} else if (message.type === 'tool_call') {
|
|
1016
|
+
const envelope = { protocol_version: DEVICE_PROTOCOL_VERSION, ...message };
|
|
1017
|
+
if (Buffer.byteLength(JSON.stringify(envelope), 'utf8') > MAX_MESSAGE_BYTES) {
|
|
1018
|
+
callback?.(deviceBrokerError('Device request exceeds maximum size.', 'DEVICE_INPUT_TOO_LARGE'));
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
const resultBase = {
|
|
1022
|
+
protocol_version: DEVICE_PROTOCOL_VERSION,
|
|
1023
|
+
request_id: message.request_id, device_id: normalizedId,
|
|
1024
|
+
connection_epoch: message.connection_epoch, timestamp: Date.now()
|
|
1025
|
+
};
|
|
1026
|
+
Promise.resolve().then(() => invoke(message.payload.tool, message.payload.arguments)).then(payload => {
|
|
1027
|
+
const result = { ...resultBase, type: 'tool_result', payload };
|
|
1028
|
+
if (Buffer.byteLength(JSON.stringify(result), 'utf8') > MAX_MESSAGE_BYTES) {
|
|
1029
|
+
throw deviceBrokerError('Gateway device response exceeds the outbound message limit', 'DEVICE_OUTPUT_TOO_LARGE');
|
|
1030
|
+
}
|
|
1031
|
+
if (endpoint.readyState === WebSocket.OPEN) handleRegisteredMessage(endpoint, result);
|
|
1032
|
+
}).catch(error => {
|
|
1033
|
+
if (endpoint.readyState !== WebSocket.OPEN) return;
|
|
1034
|
+
handleRegisteredMessage(endpoint, {
|
|
1035
|
+
...resultBase, type: 'tool_error',
|
|
1036
|
+
payload: { code: String(error?.code || 'TOOL_EXECUTION_FAILED').slice(0, 64), message: String(error?.message || error).slice(0, 4096) }
|
|
1037
|
+
});
|
|
1038
|
+
});
|
|
1039
|
+
} else if (message.type === 'device_update') {
|
|
1040
|
+
// Updating this host must restart the whole server via its service owner.
|
|
1041
|
+
// Never spawn a second Device or kill the control plane from a tool callback.
|
|
1042
|
+
const update = updateStates.get(normalizedId);
|
|
1043
|
+
if (update) {
|
|
1044
|
+
clearUpdateTimer(normalizedId);
|
|
1045
|
+
updateStates.set(normalizedId, { ...update, state: 'failed', updatedAt: Date.now(),
|
|
1046
|
+
error: { code: 'SERVER_UPDATE_REQUIRES_RESTART', message: 'Update the server package through its service owner.' } });
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
callback?.();
|
|
1050
|
+
},
|
|
1051
|
+
close(_code, reason = 'embedded Device disconnected') {
|
|
1052
|
+
if (endpoint.readyState === WebSocket.CLOSED) return;
|
|
1053
|
+
endpoint.readyState = WebSocket.CLOSED;
|
|
1054
|
+
sockets.delete(endpoint);
|
|
1055
|
+
clearTimeout(authTimer);
|
|
1056
|
+
if (!authenticated) rejectAuth(deviceBrokerError(reason, 'DEVICE_AUTH_FAILED'));
|
|
1057
|
+
const current = devices.get(normalizedId);
|
|
1058
|
+
if (!current || current.socket !== endpoint || current.connectionEpoch !== endpoint.connectionEpoch) return;
|
|
1059
|
+
current.online = false;
|
|
1060
|
+
current.socket = null;
|
|
1061
|
+
current.lastSeenAt = Date.now();
|
|
1062
|
+
recordDeviceStatus(current, 'offline');
|
|
1063
|
+
rejectPendingForConnection(normalizedId, endpoint.connectionEpoch,
|
|
1064
|
+
'Device disconnected before the request result was known; the request was not replayed.');
|
|
1065
|
+
},
|
|
1066
|
+
terminate() { endpoint.close(); }
|
|
1067
|
+
};
|
|
1068
|
+
localEndpoints.add(endpoint);
|
|
1069
|
+
sockets.add(endpoint);
|
|
1070
|
+
const authTimer = setTimeout(() => endpoint.close(4003, 'embedded identity proof timed out'), helloTimeoutMs);
|
|
1071
|
+
try {
|
|
1072
|
+
handleAuthHello(endpoint, {
|
|
1073
|
+
type: enrollmentGrant ? 'pair_hello' : 'auth_hello',
|
|
1074
|
+
device_id: normalizedId,
|
|
1075
|
+
payload: {
|
|
1076
|
+
public_key_pem: suppliedKey, capabilities,
|
|
1077
|
+
agent_version: metadata.agentVersion, package_version: metadata.packageVersion,
|
|
1078
|
+
hostname: metadata.hostname, platform: metadata.platform, arch: metadata.arch,
|
|
1079
|
+
path_style: metadata.pathStyle, runtime_ready: metadata.runtimeReady !== false,
|
|
1080
|
+
runtime_reason: metadata.runtimeReason,
|
|
1081
|
+
execution_runtime_generation: metadata.executionRuntimeGeneration
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
await authenticatedPromise;
|
|
1085
|
+
clearTimeout(authTimer);
|
|
1086
|
+
return {
|
|
1087
|
+
deviceId: normalizedId,
|
|
1088
|
+
connectionEpoch: endpoint.connectionEpoch,
|
|
1089
|
+
isOpen: () => endpoint.readyState === WebSocket.OPEN,
|
|
1090
|
+
updateCapabilities(next) {
|
|
1091
|
+
if (endpoint.readyState !== WebSocket.OPEN) return;
|
|
1092
|
+
handleRegisteredMessage(endpoint, {
|
|
1093
|
+
type: 'capability_sync', device_id: normalizedId,
|
|
1094
|
+
connection_epoch: endpoint.connectionEpoch, payload: { capabilities: next }
|
|
1095
|
+
});
|
|
1096
|
+
},
|
|
1097
|
+
close: () => endpoint.close()
|
|
1098
|
+
};
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
endpoint.close();
|
|
1101
|
+
throw error;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function attach(server, endpointPath = '/device') {
|
|
1106
|
+
if (attachedServer) throw new Error('Device broker is already attached.');
|
|
1107
|
+
attachedServer = server;
|
|
1108
|
+
upgradeHandler = (request, socket, head) => {
|
|
1109
|
+
let pathname;
|
|
1110
|
+
try { pathname = new URL(request.url || '/', 'http://localhost').pathname; }
|
|
1111
|
+
catch { return rejectUpgrade(socket, 400, 'Bad Request'); }
|
|
1112
|
+
if (pathname !== endpointPath) return;
|
|
1113
|
+
const requestedProtocols = String(request.headers['sec-websocket-protocol'] || '')
|
|
1114
|
+
.split(',')
|
|
1115
|
+
.map(value => value.trim())
|
|
1116
|
+
.filter(Boolean);
|
|
1117
|
+
const requestsV2 = requestedProtocols.includes(DEVICE_INNER_TLS_SUBPROTOCOL);
|
|
1118
|
+
if (requestsV2 && !innerTls) return rejectUpgrade(socket, 503, 'Device Inner TLS Unavailable');
|
|
1119
|
+
if (requestedProtocols.length > 0 && !requestsV2) return rejectUpgrade(socket, 400, 'Unsupported Device Protocol');
|
|
1120
|
+
if (!durableAuth) {
|
|
1121
|
+
if (!enrollmentToken) return rejectUpgrade(socket, 503, 'Device Enrollment Disabled');
|
|
1122
|
+
if (!tokenMatches(bearerToken(request), enrollmentToken)) return rejectUpgrade(socket, 401, 'Unauthorized');
|
|
1123
|
+
}
|
|
1124
|
+
wss.handleUpgrade(request, socket, head, ws => {
|
|
1125
|
+
const v2 = ws.protocol === DEVICE_INNER_TLS_SUBPROTOCOL;
|
|
1126
|
+
const credential = v2 ? '' : bearerToken(request);
|
|
1127
|
+
ws.enrollmentAuthorized = !v2 && tokenMatches(credential, enrollmentToken);
|
|
1128
|
+
ws.enrollmentCredential = ws.enrollmentAuthorized ? '' : credential;
|
|
1129
|
+
wss.emit('connection', ws, request);
|
|
1130
|
+
});
|
|
1131
|
+
};
|
|
1132
|
+
server.on('upgrade', upgradeHandler);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function listDevices({ accountId = null } = {}) {
|
|
1136
|
+
for (const device of devices.values()) {
|
|
1137
|
+
if (device.online) refreshAuthorization(device);
|
|
1138
|
+
}
|
|
1139
|
+
const owner = String(accountId || '').trim();
|
|
1140
|
+
if (durableAuth && requireAccountOwnership && !owner) return [];
|
|
1141
|
+
return [...devices.values()]
|
|
1142
|
+
.map(statusForDevice)
|
|
1143
|
+
.filter(device => !owner || device.account.account_id === owner)
|
|
1144
|
+
.sort((a, b) => a.deviceId.localeCompare(b.deviceId));
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
function requireOwnedDevice(accountId, deviceId) {
|
|
1148
|
+
if (!durableAuth) throw new Error('Durable device store is required for account-owned device operations.');
|
|
1149
|
+
const owner = String(accountId || '').trim();
|
|
1150
|
+
const normalized = normalizeDeviceId(deviceId);
|
|
1151
|
+
if (!owner) throw deviceBrokerError('ACCOUNT_ID_REQUIRED: account_id is required for device management.', 'ACCOUNT_ID_REQUIRED');
|
|
1152
|
+
const stored = deviceStore.get(normalized);
|
|
1153
|
+
if (!stored || !stored.ownerAccountId || stored.ownerAccountId !== owner) {
|
|
1154
|
+
throw deviceBrokerError('DEVICE_ACCESS_DENIED: device is not owned by the caller account.', 'DEVICE_ACCESS_DENIED');
|
|
1155
|
+
}
|
|
1156
|
+
return stored;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function renameOwnedDevice({ accountId, deviceId, deviceName }) {
|
|
1160
|
+
const stored = requireOwnedDevice(accountId, deviceId);
|
|
1161
|
+
const name = String(deviceName || '').trim();
|
|
1162
|
+
if (!name || name.length > 128) throw new Error('device_name must be between 1 and 128 characters.');
|
|
1163
|
+
deviceStore.updateMetadata({ deviceId: stored.deviceId, deviceName: name });
|
|
1164
|
+
const current = devices.get(stored.deviceId) || { deviceId: stored.deviceId, connectionEpoch: 0, capabilities: [] };
|
|
1165
|
+
if (current.online) sendStatusSnapshot(current.socket, current);
|
|
1166
|
+
return statusForDevice(current);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function revokeDevice(deviceId, { accountId = null, callerCategory = 'system' } = {}) {
|
|
1170
|
+
if (!durableAuth) throw new Error('Durable device store is required for revocation.');
|
|
1171
|
+
const normalized = normalizeDeviceId(deviceId);
|
|
1172
|
+
const stored = deviceStore.get(normalized);
|
|
1173
|
+
if (!stored) throw new Error(`Unknown device ${normalized}.`);
|
|
1174
|
+
const current = devices.get(normalized) || { deviceId: normalized, connectionEpoch: 0, capabilities: [] };
|
|
1175
|
+
|
|
1176
|
+
deviceStore.revoke(normalized);
|
|
1177
|
+
if (current.socket) {
|
|
1178
|
+
rejectPendingForConnection(
|
|
1179
|
+
normalized,
|
|
1180
|
+
current.connectionEpoch,
|
|
1181
|
+
'Device was forgotten before the request result was known; the request was not replayed.'
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
clearUpdateTimer(normalized);
|
|
1185
|
+
updateStates.delete(normalized);
|
|
1186
|
+
activityStates.delete(normalized);
|
|
1187
|
+
devices.delete(normalized);
|
|
1188
|
+
try {
|
|
1189
|
+
onDeviceForgotten?.({ deviceId: normalized, accountId: stored.ownerAccountId || null });
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
console.error(`[device-broker] device cleanup callback failed: ${error.message}`);
|
|
1192
|
+
}
|
|
1193
|
+
if (current.socket?.readyState === WebSocket.OPEN) current.socket.close(4004, 'device forgotten');
|
|
1194
|
+
auditRecorder?.recordEvent?.({
|
|
1195
|
+
event: 'device_forgotten',
|
|
1196
|
+
callerId: accountId || stored.ownerAccountId || '',
|
|
1197
|
+
callerCategory,
|
|
1198
|
+
deviceId: normalized,
|
|
1199
|
+
details: {
|
|
1200
|
+
deviceName: stored.deviceName || normalized,
|
|
1201
|
+
hostname: stored.hostname || null,
|
|
1202
|
+
packageVersion: stored.packageVersion || null
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
return { deviceId: normalized, deviceName: stored.deviceName || normalized, forgotten: true };
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function revokeOwnedDevice({ accountId, deviceId }) {
|
|
1209
|
+
const stored = requireOwnedDevice(accountId, deviceId);
|
|
1210
|
+
return revokeDevice(stored.deviceId, { accountId, callerCategory: 'dashboard' });
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function requestDeviceUpdate({ accountId, deviceId, targetVersion }) {
|
|
1214
|
+
const stored = requireOwnedDevice(accountId, deviceId);
|
|
1215
|
+
const device = devices.get(stored.deviceId);
|
|
1216
|
+
if (device?.online && !refreshAuthorization(device)) {
|
|
1217
|
+
throw deviceBrokerError(`Device ${stored.deviceId} authorization changed or was revoked; it is offline.`, 'DEVICE_OFFLINE');
|
|
1218
|
+
}
|
|
1219
|
+
if (!device?.online || device.revoked || !device.socket || device.socket.readyState !== WebSocket.OPEN) {
|
|
1220
|
+
throw deviceBrokerError(`Device ${stored.deviceId} is offline.`, 'DEVICE_OFFLINE');
|
|
1221
|
+
}
|
|
1222
|
+
const target = normalizeStableVersion(targetVersion);
|
|
1223
|
+
let current;
|
|
1224
|
+
try { current = normalizeStableVersion(device.packageVersion || stored.packageVersion); }
|
|
1225
|
+
catch { throw deviceBrokerError('Device package version is unavailable; bootstrap MCP Device before dashboard updates.', 'DEVICE_UPDATE_BOOTSTRAP_REQUIRED'); }
|
|
1226
|
+
const platform = device.platform || stored.platform || null;
|
|
1227
|
+
const minimumVersion = minimumSelfUpdateVersion(platform);
|
|
1228
|
+
if (!supportsDeviceSelfUpdate(current, platform)) {
|
|
1229
|
+
throw deviceBrokerError(`Device must be bootstrapped to MCP Device ${minimumVersion} before dashboard self-update is available.`, 'DEVICE_UPDATE_BOOTSTRAP_REQUIRED');
|
|
1230
|
+
}
|
|
1231
|
+
if (compareStableVersions(target, current) <= 0) {
|
|
1232
|
+
throw deviceBrokerError(`Device is already on ${current} or newer.`, 'DEVICE_UPDATE_NOT_NEEDED');
|
|
1233
|
+
}
|
|
1234
|
+
const existing = updateStates.get(device.deviceId);
|
|
1235
|
+
if (existing && ['requested', 'accepted', 'installed'].includes(existing.state)) {
|
|
1236
|
+
if (existing.targetVersion === target) return existing;
|
|
1237
|
+
throw deviceBrokerError('Another device update is already in progress.', 'DEVICE_UPDATE_IN_PROGRESS');
|
|
1238
|
+
}
|
|
1239
|
+
const requestId = randomUUID();
|
|
1240
|
+
const update = {
|
|
1241
|
+
requestId,
|
|
1242
|
+
targetVersion: target,
|
|
1243
|
+
state: 'requested',
|
|
1244
|
+
requestedAt: Date.now(),
|
|
1245
|
+
updatedAt: Date.now(),
|
|
1246
|
+
error: null
|
|
1247
|
+
};
|
|
1248
|
+
const wireMessage = {
|
|
1249
|
+
type: 'device_update',
|
|
1250
|
+
request_id: requestId,
|
|
1251
|
+
device_id: device.deviceId,
|
|
1252
|
+
connection_epoch: device.connectionEpoch,
|
|
1253
|
+
timestamp: Date.now(),
|
|
1254
|
+
payload: { target_version: target }
|
|
1255
|
+
};
|
|
1256
|
+
const dispatch = () => {
|
|
1257
|
+
updateStates.set(device.deviceId, update);
|
|
1258
|
+
const sent = sendDeviceMessage(device.socket, wireMessage, error => {
|
|
1259
|
+
if (!error) return;
|
|
1260
|
+
const currentUpdate = updateStates.get(device.deviceId);
|
|
1261
|
+
if (currentUpdate?.requestId !== requestId) return;
|
|
1262
|
+
clearUpdateTimer(device.deviceId);
|
|
1263
|
+
updateStates.set(device.deviceId, {
|
|
1264
|
+
...currentUpdate,
|
|
1265
|
+
state: 'failed',
|
|
1266
|
+
updatedAt: Date.now(),
|
|
1267
|
+
error: { code: 'DEVICE_UPDATE_SEND_ERROR', message: String(error.message || error).slice(0, 240) }
|
|
1268
|
+
});
|
|
1269
|
+
});
|
|
1270
|
+
if (!sent) {
|
|
1271
|
+
clearUpdateTimer(device.deviceId);
|
|
1272
|
+
updateStates.set(device.deviceId, {
|
|
1273
|
+
...update,
|
|
1274
|
+
state: 'failed',
|
|
1275
|
+
updatedAt: Date.now(),
|
|
1276
|
+
error: { code: 'DEVICE_UPDATE_SEND_ERROR', message: 'Device update could not be sent.' }
|
|
1277
|
+
});
|
|
1278
|
+
throw deviceBrokerError('Device update could not be sent.', 'DEVICE_UPDATE_SEND_ERROR');
|
|
1279
|
+
}
|
|
1280
|
+
armUpdateTimer(device.deviceId, requestId);
|
|
1281
|
+
};
|
|
1282
|
+
deviceStore.withCurrentAuthorization(
|
|
1283
|
+
{
|
|
1284
|
+
deviceId: device.deviceId,
|
|
1285
|
+
publicKeyPem: device.authenticatedPublicKeyPem,
|
|
1286
|
+
authorizationGeneration: device.authenticatedAuthorizationGeneration
|
|
1287
|
+
},
|
|
1288
|
+
dispatch
|
|
1289
|
+
);
|
|
1290
|
+
return updateStates.get(device.deviceId);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
async function callDevice({
|
|
1294
|
+
requestId: requestedRequestId,
|
|
1295
|
+
accountId = null,
|
|
1296
|
+
deviceId,
|
|
1297
|
+
tool,
|
|
1298
|
+
arguments: args = {},
|
|
1299
|
+
timeoutMs = requestTimeoutMs,
|
|
1300
|
+
expectedConnectionEpoch,
|
|
1301
|
+
expectedExecutionRuntimeGeneration,
|
|
1302
|
+
includeDispatchContext = false
|
|
1303
|
+
}) {
|
|
1304
|
+
const device = devices.get(String(deviceId || ''));
|
|
1305
|
+
if (device?.online && !refreshAuthorization(device)) {
|
|
1306
|
+
throw deviceBrokerError(`Device ${deviceId} authorization changed or was revoked; it is offline.`, 'DEVICE_OFFLINE');
|
|
1307
|
+
}
|
|
1308
|
+
if (!device?.online || device.revoked || !device.socket || device.socket.readyState !== WebSocket.OPEN) {
|
|
1309
|
+
throw deviceBrokerError(`Device ${deviceId} is offline or unknown.`, 'DEVICE_OFFLINE');
|
|
1310
|
+
}
|
|
1311
|
+
const currentRuntimeGeneration = effectiveRuntimeGeneration(device);
|
|
1312
|
+
if (expectedConnectionEpoch !== undefined && String(expectedConnectionEpoch) !== String(device.connectionEpoch)) {
|
|
1313
|
+
throw deviceBrokerError('Process session is stale because the device connection changed.', 'PROCESS_SESSION_STALE');
|
|
1314
|
+
}
|
|
1315
|
+
if (expectedExecutionRuntimeGeneration !== undefined &&
|
|
1316
|
+
String(expectedExecutionRuntimeGeneration) !== currentRuntimeGeneration) {
|
|
1317
|
+
throw deviceBrokerError('Process session is stale because the execution runtime changed.', 'PROCESS_SESSION_STALE');
|
|
1318
|
+
}
|
|
1319
|
+
if (device.runtimeReady === false) {
|
|
1320
|
+
throw deviceBrokerError(
|
|
1321
|
+
`Device ${deviceId} execution runtime is not ready${device.runtimeReason ? `: ${device.runtimeReason}` : '.'}`,
|
|
1322
|
+
'DEVICE_NOT_READY'
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
if (durableAuth && requireAccountOwnership) {
|
|
1326
|
+
const owner = String(accountId || '').trim();
|
|
1327
|
+
const stored = deviceStore.get(device.deviceId);
|
|
1328
|
+
if (!owner) throw new Error('ACCOUNT_ID_REQUIRED: account_id is required for remote device dispatch.');
|
|
1329
|
+
if (!stored?.ownerAccountId || stored.ownerAccountId !== owner) throw new Error('DEVICE_ACCESS_DENIED: device is not owned by the caller account.');
|
|
1330
|
+
}
|
|
1331
|
+
if (!device.capabilities.includes(tool)) throw new Error(`Device ${deviceId} does not advertise capability ${tool}.`);
|
|
1332
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30000) throw new Error('Device request timeout must be an integer between 1 and 30000 ms.');
|
|
1333
|
+
const requestId = requestedRequestId === undefined ? randomUUID() : String(requestedRequestId).trim();
|
|
1334
|
+
if (!requestId || requestId.length > 128) throw new Error('Device request_id must be between 1 and 128 characters.');
|
|
1335
|
+
if (pending.has(requestId)) throw new Error(`Device request ${requestId} is already pending.`);
|
|
1336
|
+
const wireRequestId = randomUUID();
|
|
1337
|
+
const wireMessage = {
|
|
1338
|
+
type: 'tool_call',
|
|
1339
|
+
request_id: wireRequestId,
|
|
1340
|
+
device_id: device.deviceId,
|
|
1341
|
+
connection_epoch: device.connectionEpoch,
|
|
1342
|
+
timestamp: Date.now(),
|
|
1343
|
+
payload: { tool, arguments: args }
|
|
1344
|
+
};
|
|
1345
|
+
const wireVersion = socketProtocolVersion(device.socket);
|
|
1346
|
+
const requestBytes = Buffer.byteLength(JSON.stringify({ ...wireMessage, protocol_version: wireVersion }), 'utf8');
|
|
1347
|
+
return await new Promise((resolve, reject) => {
|
|
1348
|
+
let entry = null;
|
|
1349
|
+
const dispatch = () => {
|
|
1350
|
+
const current = devices.get(device.deviceId);
|
|
1351
|
+
if (current !== device || !device.online || device.revoked ||
|
|
1352
|
+
!device.socket || device.socket.readyState !== WebSocket.OPEN) {
|
|
1353
|
+
if (expectedConnectionEpoch !== undefined || expectedExecutionRuntimeGeneration !== undefined) {
|
|
1354
|
+
throw deviceBrokerError('Process session is stale because the device connection changed.', 'PROCESS_SESSION_STALE');
|
|
1355
|
+
}
|
|
1356
|
+
throw deviceBrokerError(`Device ${deviceId} connection changed before dispatch.`, 'DEVICE_OFFLINE');
|
|
1357
|
+
}
|
|
1358
|
+
const dispatchRuntimeGeneration = effectiveRuntimeGeneration(device);
|
|
1359
|
+
if (expectedConnectionEpoch !== undefined && String(expectedConnectionEpoch) !== String(device.connectionEpoch)) {
|
|
1360
|
+
throw deviceBrokerError('Process session is stale because the device connection changed.', 'PROCESS_SESSION_STALE');
|
|
1361
|
+
}
|
|
1362
|
+
if (expectedExecutionRuntimeGeneration !== undefined &&
|
|
1363
|
+
String(expectedExecutionRuntimeGeneration) !== dispatchRuntimeGeneration) {
|
|
1364
|
+
throw deviceBrokerError('Process session is stale because the execution runtime changed.', 'PROCESS_SESSION_STALE');
|
|
1365
|
+
}
|
|
1366
|
+
if (device.runtimeReady === false) {
|
|
1367
|
+
throw deviceBrokerError(
|
|
1368
|
+
`Device ${deviceId} execution runtime is not ready${device.runtimeReason ? `: ${device.runtimeReason}` : '.'}`,
|
|
1369
|
+
'DEVICE_NOT_READY'
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
const dispatchContext = {
|
|
1373
|
+
deviceId: device.deviceId,
|
|
1374
|
+
connectionEpoch: device.connectionEpoch,
|
|
1375
|
+
executionRuntimeGeneration: dispatchRuntimeGeneration
|
|
1376
|
+
};
|
|
1377
|
+
entry = {
|
|
1378
|
+
requestId, wireRequestId, resolve, reject, timer: null,
|
|
1379
|
+
deviceId: device.deviceId, connectionEpoch: device.connectionEpoch,
|
|
1380
|
+
executionRuntimeGeneration: dispatchRuntimeGeneration,
|
|
1381
|
+
includeDispatchContext: includeDispatchContext === true,
|
|
1382
|
+
dispatchContext,
|
|
1383
|
+
tool, startedAt: Date.now()
|
|
1384
|
+
};
|
|
1385
|
+
const timer = setTimeout(() => {
|
|
1386
|
+
if (pending.get(requestId) !== entry) return;
|
|
1387
|
+
pending.delete(requestId);
|
|
1388
|
+
if (pendingByWireRequestId.get(wireRequestId) === entry) pendingByWireRequestId.delete(wireRequestId);
|
|
1389
|
+
usageStore?.recordToolFailed(device.deviceId, 0, 'DEVICE_REQUEST_TIMEOUT');
|
|
1390
|
+
noteActivityFinished(device.deviceId, tool, 'timeout');
|
|
1391
|
+
entry.reject(deviceBrokerError(`Device request ${requestId} timed out; it was not replayed.`, 'DEVICE_REQUEST_TIMEOUT'));
|
|
1392
|
+
}, timeoutMs);
|
|
1393
|
+
entry.timer = timer;
|
|
1394
|
+
timer.unref?.();
|
|
1395
|
+
pending.set(requestId, entry);
|
|
1396
|
+
pendingByWireRequestId.set(wireRequestId, entry);
|
|
1397
|
+
noteActivitySent(device.deviceId, tool);
|
|
1398
|
+
sendDeviceMessage(device.socket, wireMessage, error => {
|
|
1399
|
+
if (!error || pending.get(requestId) !== entry) return;
|
|
1400
|
+
clearTimeout(entry.timer);
|
|
1401
|
+
pending.delete(requestId);
|
|
1402
|
+
if (pendingByWireRequestId.get(wireRequestId) === entry) pendingByWireRequestId.delete(wireRequestId);
|
|
1403
|
+
usageStore?.recordToolFailed(device.deviceId, 0, 'DEVICE_SEND_ERROR');
|
|
1404
|
+
noteActivityFinished(device.deviceId, tool, 'send_error');
|
|
1405
|
+
entry.reject(error);
|
|
1406
|
+
});
|
|
1407
|
+
};
|
|
1408
|
+
try {
|
|
1409
|
+
if (durableAuth) {
|
|
1410
|
+
deviceStore.withCurrentAuthorization(
|
|
1411
|
+
{
|
|
1412
|
+
deviceId: device.deviceId,
|
|
1413
|
+
publicKeyPem: device.authenticatedPublicKeyPem,
|
|
1414
|
+
authorizationGeneration: device.authenticatedAuthorizationGeneration
|
|
1415
|
+
},
|
|
1416
|
+
dispatch
|
|
1417
|
+
);
|
|
1418
|
+
} else dispatch();
|
|
1419
|
+
usageStore?.recordToolStarted(device.deviceId, requestBytes);
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
if (entry && pending.get(requestId) === entry) {
|
|
1422
|
+
clearTimeout(entry.timer);
|
|
1423
|
+
pending.delete(requestId);
|
|
1424
|
+
if (pendingByWireRequestId.get(wireRequestId) === entry) pendingByWireRequestId.delete(wireRequestId);
|
|
1425
|
+
}
|
|
1426
|
+
if (durableAuth && /authorization changed|revoked|stale/i.test(String(error?.message || ''))) {
|
|
1427
|
+
invalidateAuthorization(device, { revoked: false, reason: 'Device authorization changed', closeCode: 4005 });
|
|
1428
|
+
}
|
|
1429
|
+
reject(error);
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
async function shutdown() {
|
|
1435
|
+
if (attachedServer && upgradeHandler) attachedServer.off('upgrade', upgradeHandler);
|
|
1436
|
+
for (const entry of pending.values()) {
|
|
1437
|
+
clearTimeout(entry.timer);
|
|
1438
|
+
if (pending.get(entry.requestId) === entry) pending.delete(entry.requestId);
|
|
1439
|
+
if (pendingByWireRequestId.get(entry.wireRequestId) === entry) pendingByWireRequestId.delete(entry.wireRequestId);
|
|
1440
|
+
entry.reject(new Error('Device broker is shutting down.'));
|
|
1441
|
+
}
|
|
1442
|
+
pending.clear();
|
|
1443
|
+
pendingByWireRequestId.clear();
|
|
1444
|
+
for (const deviceId of updateTimers.keys()) clearUpdateTimer(deviceId);
|
|
1445
|
+
for (const ws of sockets) { try { ws.terminate(); } catch {} }
|
|
1446
|
+
sockets.clear();
|
|
1447
|
+
await new Promise(resolve => wss.close(() => resolve()));
|
|
1448
|
+
attachedServer = null;
|
|
1449
|
+
upgradeHandler = null;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function setSchemaSnapshot(value) {
|
|
1453
|
+
schemaSnapshot = normalizeSchemaSnapshot(value);
|
|
1454
|
+
for (const device of devices.values()) {
|
|
1455
|
+
if (device.online) sendStatusSnapshot(device.socket, device);
|
|
1456
|
+
}
|
|
1457
|
+
return schemaSnapshot;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
return { attach, attachLocalDevice, listDevices, getActivitySnapshot, renameOwnedDevice, revokeDevice, revokeOwnedDevice, requestDeviceUpdate, callDevice, setSchemaSnapshot, shutdown };
|
|
1461
|
+
}
|