clawmini 0.0.7 → 0.0.9
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/.changeset/README.md +8 -0
- package/.changeset/config.json +14 -0
- package/.github/workflows/release.yml +49 -0
- package/CHANGELOG.md +36 -0
- package/README.md +5 -4
- package/dist/adapter-discord/index.d.mts.map +1 -1
- package/dist/adapter-discord/index.mjs +465 -282
- package/dist/adapter-discord/index.mjs.map +1 -1
- package/dist/adapter-google-chat/index.mjs +367 -243
- package/dist/adapter-google-chat/index.mjs.map +1 -1
- package/dist/cli/index.mjs +684 -24
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/lite.mjs +43 -13
- package/dist/cli/lite.mjs.map +1 -1
- package/dist/cli/{propose-policy.mjs → manage-policies.mjs} +270 -47
- package/dist/cli/manage-policies.mjs.map +1 -0
- package/dist/cli/run-host.d.mts +1 -0
- package/dist/cli/run-host.mjs +3090 -0
- package/dist/cli/run-host.mjs.map +1 -0
- package/dist/config-CPFQIGdG.mjs +57 -0
- package/dist/config-CPFQIGdG.mjs.map +1 -0
- package/dist/config-Dvl-Pov4.mjs +76 -0
- package/dist/config-Dvl-Pov4.mjs.map +1 -0
- package/dist/daemon/index.d.mts.map +1 -1
- package/dist/daemon/index.mjs +970 -332
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/supervisor-actions-CiW56eLi.mjs +843 -0
- package/dist/supervisor-actions-CiW56eLi.mjs.map +1 -0
- package/dist/turn-log-buffer-DRgW53gl.mjs +767 -0
- package/dist/turn-log-buffer-DRgW53gl.mjs.map +1 -0
- package/dist/web/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
- package/dist/web/_app/immutable/chunks/BhRSsUCh.js +2 -0
- package/dist/web/_app/immutable/chunks/BiLeM2i1.js +1 -0
- package/{web/.svelte-kit/output/client/_app/immutable/chunks/CME08kGM.js → dist/web/_app/immutable/chunks/BmBj85Ll.js} +1 -1
- package/dist/web/_app/immutable/chunks/BrERcKAH.js +1 -0
- package/dist/web/_app/immutable/chunks/Bv9252RM.js +1 -0
- package/dist/web/_app/immutable/chunks/CIXNBPKi.js +1 -0
- package/dist/web/_app/immutable/chunks/DISKL3GN.js +2 -0
- package/dist/web/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
- package/dist/web/_app/immutable/chunks/DnQ3vS13.js +1 -0
- package/dist/web/_app/immutable/chunks/KsloHTKS.js +1 -0
- package/{web/.svelte-kit/output/client/_app/immutable/chunks/Ck-be5J2.js → dist/web/_app/immutable/chunks/RsHsUj-8.js} +2 -2
- package/dist/web/_app/immutable/chunks/{G_zz-Gou.js → wpfV79dV.js} +1 -1
- package/dist/web/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
- package/dist/web/_app/immutable/entry/start.Di0-Jhte.js +1 -0
- package/dist/web/_app/immutable/nodes/{0.CYS8iApT.js → 0.DYyUA1au.js} +1 -1
- package/dist/web/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
- package/dist/web/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
- package/{web/.svelte-kit/output/client/_app/immutable/nodes/3.Dr0ot9sV.js → dist/web/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
- package/dist/web/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
- package/dist/web/_app/immutable/nodes/{5.BBGQ_i84.js → 5.DLbPVJY2.js} +1 -1
- package/dist/web/_app/version.json +1 -1
- package/dist/web/index.html +12 -12
- package/dist/workspace-oWmVh5mi.mjs +1001 -0
- package/dist/workspace-oWmVh5mi.mjs.map +1 -0
- package/docs/23_adapter_slash_autocomplete/development_log.md +19 -0
- package/docs/23_adapter_slash_autocomplete/notes.md +18 -0
- package/docs/23_adapter_slash_autocomplete/prd.md +46 -0
- package/docs/23_adapter_slash_autocomplete/questions.md +6 -0
- package/docs/23_adapter_slash_autocomplete/tickets.md +21 -0
- package/docs/24_subagent_job_policy_fixes/development_log.md +22 -0
- package/docs/24_subagent_job_policy_fixes/notes.md +28 -0
- package/docs/24_subagent_job_policy_fixes/prd.md +59 -0
- package/docs/24_subagent_job_policy_fixes/questions.md +3 -0
- package/docs/24_subagent_job_policy_fixes/tickets.md +49 -0
- package/docs/25_e2e_test_improvements/development_log.md +30 -0
- package/docs/25_e2e_test_improvements/notes.md +29 -0
- package/docs/25_e2e_test_improvements/prd.md +43 -0
- package/docs/25_e2e_test_improvements/questions.md +12 -0
- package/docs/25_e2e_test_improvements/tickets-2.md +22 -0
- package/docs/25_e2e_test_improvements/tickets.md +22 -0
- package/docs/25_policy_cwd/development_log.md +30 -0
- package/docs/25_policy_cwd/notes.md +28 -0
- package/docs/25_policy_cwd/prd.md +77 -0
- package/docs/25_policy_cwd/questions.md +6 -0
- package/docs/25_policy_cwd/tickets.md +77 -0
- package/docs/CLI_REFERENCE.md +3 -1
- package/docs/PHILOSOPHY.md +35 -0
- package/docs/adapter-visibility/SPEC.md +461 -0
- package/docs/adapter-visibility/SPEC_v2.md +202 -0
- package/docs/auto-update/SPEC.md +344 -0
- package/docs/backups/SPEC.md +296 -0
- package/docs/backups/clawmini.gitignore +69 -0
- package/docs/guides/assets/clawmini-avatar.png +0 -0
- package/docs/guides/backups.md +332 -0
- package/docs/guides/discord_adapter_setup.md +1 -1
- package/docs/guides/google_chat_adapter_setup.md +81 -0
- package/docs/unified-startup/SPEC.md +203 -0
- package/e2e/_helpers/test-environment.test.ts +49 -0
- package/e2e/_helpers/test-environment.ts +548 -0
- package/e2e/adapters/_google-chat-fixtures.ts +340 -0
- package/{src/cli/e2e → e2e/adapters}/adapter-discord.test.ts +22 -23
- package/e2e/adapters/adapter-google-chat-downtime.test.ts +157 -0
- package/e2e/adapters/adapter-google-chat-inbound.test.ts +697 -0
- package/e2e/adapters/adapter-google-chat-outbound.test.ts +297 -0
- package/e2e/adapters/adapter-google-chat-roundtrip.test.ts +56 -0
- package/e2e/adapters/adapter-google-chat-threads.test.ts +1078 -0
- package/e2e/agents/custom-api-env.test.ts +80 -0
- package/e2e/agents/export-lite-func.test.ts +104 -0
- package/e2e/agents/fallbacks.test.ts +124 -0
- package/e2e/agents/interrupt.test.ts +50 -0
- package/e2e/agents/no-reply-necessary.test.ts +57 -0
- package/e2e/agents/session-timeout-subagents.test.ts +76 -0
- package/e2e/agents/subagent-authorization.test.ts +246 -0
- package/e2e/agents/subagent-env.test.ts +49 -0
- package/e2e/agents/subagent-lifecycle.test.ts +782 -0
- package/e2e/agents/subagents-depth.test.ts +47 -0
- package/e2e/cli/agents.test.ts +176 -0
- package/e2e/cli/auto-update.test.ts +741 -0
- package/e2e/cli/basic.test.ts +44 -0
- package/{src/cli/e2e → e2e/cli}/export-lite.test.ts +16 -12
- package/e2e/cli/init-gitignore.test.ts +86 -0
- package/e2e/cli/init.test.ts +76 -0
- package/e2e/cli/messages.test.ts +363 -0
- package/e2e/cli/serve.test.ts +76 -0
- package/{src/cli/e2e → e2e/cli}/skills.test.ts +11 -10
- package/{src/cli/e2e → e2e/daemon}/daemon.test.ts +57 -195
- package/e2e/jobs/agent-jobs.test.ts +216 -0
- package/e2e/jobs/cron.test.ts +64 -0
- package/e2e/jobs/restart.test.ts +108 -0
- package/e2e/policies/approval-session.test.ts +69 -0
- package/e2e/policies/auto-create-policies-file.test.ts +35 -0
- package/e2e/policies/builtin-manage-policies.test.ts +184 -0
- package/e2e/policies/builtin-run-host.test.ts +180 -0
- package/e2e/policies/environment-policies.test.ts +177 -0
- package/e2e/policies/manage-policies.test.ts +566 -0
- package/e2e/policies/output-size.test.ts +98 -0
- package/e2e/policies/policies-context-cwd.test.ts +160 -0
- package/e2e/policies/relative-script-path.test.ts +60 -0
- package/e2e/policies/requests-show.test.ts +135 -0
- package/e2e/policies/requests.test.ts +208 -0
- package/e2e/policies/slash-policies.test.ts +308 -0
- package/e2e/policies/startup-cleanup.test.ts +48 -0
- package/e2e/routers/session-timeout.test.ts +106 -0
- package/e2e/routers/slash-model.test.ts +152 -0
- package/e2e/routers/slash-new.test.ts +50 -0
- package/e2e/routers/slash-restart-adapter.test.ts +96 -0
- package/e2e/routers/slash-restart.test.ts +114 -0
- package/e2e/routers/slash-shutdown.test.ts +55 -0
- package/e2e/routers/slash-stop.test.ts +232 -0
- package/e2e/routers/slash-upgrade.test.ts +88 -0
- package/{src/cli/e2e → e2e/sandbox}/environments.test.ts +14 -13
- package/eslint.config.js +6 -0
- package/napkin.md +1 -1
- package/package.json +8 -3
- package/src/adapter-discord/commands.test.ts +42 -0
- package/src/adapter-discord/commands.ts +33 -0
- package/src/adapter-discord/config.ts +12 -0
- package/src/adapter-discord/forwarder.test.ts +499 -21
- package/src/adapter-discord/forwarder.ts +343 -124
- package/src/adapter-discord/inbound-cache.test.ts +47 -0
- package/src/adapter-discord/inbound-cache.ts +37 -0
- package/src/adapter-discord/index.test.ts +67 -2
- package/src/adapter-discord/index.ts +84 -216
- package/src/adapter-discord/interactions.test.ts +54 -3
- package/src/adapter-discord/interactions.ts +97 -53
- package/src/adapter-discord/processMessage.ts +239 -0
- package/src/adapter-discord/state.ts +1 -0
- package/src/adapter-google-chat/auth.test.ts +9 -5
- package/src/adapter-google-chat/auth.ts +29 -23
- package/src/adapter-google-chat/cards.ts +7 -2
- package/src/adapter-google-chat/client.test.ts +37 -2
- package/src/adapter-google-chat/client.ts +138 -38
- package/src/adapter-google-chat/config.ts +19 -0
- package/src/adapter-google-chat/forwarder.test.ts +81 -56
- package/src/adapter-google-chat/forwarder.ts +394 -185
- package/src/adapter-google-chat/inbound-cache.test.ts +61 -0
- package/src/adapter-google-chat/inbound-cache.ts +36 -0
- package/src/adapter-google-chat/state.test.ts +1 -0
- package/src/adapter-google-chat/state.ts +9 -1
- package/src/adapter-google-chat/subscriptions.ts +8 -6
- package/src/cli/builtin-policies.ts +44 -0
- package/src/cli/commands/agents.ts +59 -5
- package/src/cli/commands/down.ts +54 -2
- package/src/cli/commands/environments.ts +8 -2
- package/src/cli/commands/init.ts +31 -0
- package/src/cli/commands/logs.ts +116 -0
- package/src/cli/commands/policies.ts +6 -4
- package/src/cli/commands/serve.test.ts +67 -0
- package/src/cli/commands/serve.ts +284 -0
- package/src/cli/commands/up.ts +122 -2
- package/src/cli/commands/web-api/agents.ts +3 -2
- package/src/cli/index.ts +4 -0
- package/src/cli/install-detection.test.ts +72 -0
- package/src/cli/install-detection.ts +48 -0
- package/src/cli/lite.ts +54 -22
- package/src/cli/manage-policies-utils.ts +104 -0
- package/src/cli/manage-policies.ts +291 -0
- package/src/cli/run-host.ts +45 -0
- package/src/cli/supervisor-actions.ts +267 -0
- package/src/cli/supervisor-control.test.ts +129 -0
- package/src/cli/supervisor-control.ts +155 -0
- package/src/cli/supervisor-pid.ts +68 -0
- package/src/cli/supervisor.ts +277 -0
- package/src/daemon/agent/agent-context.ts +11 -11
- package/src/daemon/agent/agent-session.ts +8 -1
- package/src/daemon/agent/chat-logger.test.ts +78 -9
- package/src/daemon/agent/chat-logger.ts +25 -5
- package/src/daemon/agent/turn-registry.test.ts +89 -0
- package/src/daemon/agent/turn-registry.ts +94 -0
- package/src/daemon/agent/types.ts +2 -0
- package/src/daemon/api/agent-policy-endpoints.ts +263 -0
- package/src/daemon/api/agent-router.ts +47 -126
- package/src/daemon/api/index.test.ts +1 -0
- package/src/daemon/api/policy-request.test.ts +7 -5
- package/src/daemon/api/router-utils.ts +6 -5
- package/src/daemon/api/subagent-router.ts +110 -74
- package/src/daemon/api/subagent-utils.test.ts +60 -0
- package/src/daemon/api/subagent-utils.ts +113 -87
- package/src/daemon/api/user-router.ts +34 -8
- package/src/daemon/auth.ts +1 -0
- package/src/daemon/cron.test.ts +62 -4
- package/src/daemon/cron.ts +42 -16
- package/src/daemon/events.ts +65 -0
- package/src/daemon/index.ts +24 -1
- package/src/daemon/message-interruption.test.ts +1 -0
- package/src/daemon/message-jobs.test.ts +1 -0
- package/src/daemon/message.ts +78 -14
- package/src/daemon/observation.test.ts +26 -18
- package/src/daemon/pending-replies.test.ts +112 -0
- package/src/daemon/pending-replies.ts +162 -0
- package/src/daemon/policy-request-service.ts +3 -1
- package/src/daemon/policy-utils.test.ts +66 -1
- package/src/daemon/policy-utils.ts +126 -1
- package/src/daemon/request-store.ts +31 -0
- package/src/daemon/routers/session-timeout.ts +4 -0
- package/src/daemon/routers/slash-model.test.ts +344 -0
- package/src/daemon/routers/slash-model.ts +207 -0
- package/src/daemon/routers/slash-policies.test.ts +38 -32
- package/src/daemon/routers/slash-policies.ts +84 -33
- package/src/daemon/routers/slash-restart.test.ts +69 -0
- package/src/daemon/routers/slash-restart.ts +36 -0
- package/src/daemon/routers/slash-shutdown.test.ts +50 -0
- package/src/daemon/routers/slash-shutdown.ts +28 -0
- package/src/daemon/routers/slash-upgrade.test.ts +116 -0
- package/src/daemon/routers/slash-upgrade.ts +76 -0
- package/src/daemon/routers/types.ts +7 -0
- package/src/daemon/routers.ts +16 -0
- package/src/shared/adapters/blockquote.test.ts +28 -0
- package/src/shared/adapters/blockquote.ts +20 -0
- package/src/shared/adapters/filtering.test.ts +224 -10
- package/src/shared/adapters/filtering.ts +95 -7
- package/src/shared/adapters/inbound-cache.test.ts +48 -0
- package/src/shared/adapters/inbound-cache.ts +54 -0
- package/src/shared/adapters/turn-log-buffer.ts +266 -0
- package/src/shared/adapters/turn-log.test.ts +389 -0
- package/src/shared/adapters/turn-log.ts +357 -0
- package/src/shared/agent-utils.ts +12 -5
- package/src/shared/chats.test.ts +4 -0
- package/src/shared/chats.ts +9 -0
- package/src/shared/config.ts +16 -1
- package/src/shared/lite.ts +76 -2
- package/src/shared/policies.ts +26 -0
- package/src/shared/template-manifest.ts +267 -0
- package/src/shared/utils/shell.ts +61 -0
- package/src/shared/version.ts +34 -0
- package/src/shared/workspace.test.ts +217 -0
- package/src/shared/workspace.ts +626 -48
- package/templates/environments/cladding/allowlist-domain.mjs +125 -0
- package/templates/environments/cladding/env.json +21 -1
- package/templates/environments/cladding/run-with-network.mjs +54 -0
- package/templates/environments/macos-proxy/allowlist-domain.mjs +95 -0
- package/templates/environments/macos-proxy/env.json +8 -1
- package/templates/environments/macos-proxy/proxy.mjs +42 -13
- package/templates/gemini/template.json +5 -0
- package/templates/gemini-claw/template.json +13 -0
- package/templates/skills/clawmini-requests/SKILL.md +69 -10
- package/templates/skills/run-host/SKILL.md +51 -0
- package/templates/skills/skill-creator/SKILL.md +4 -3
- package/templates/skills/skill-creator/scripts/validate.sh +52 -0
- package/tsdown.config.ts +10 -1
- package/vitest.config.ts +2 -2
- package/web/.svelte-kit/ambient.d.ts +292 -176
- package/web/.svelte-kit/generated/server/internal.js +1 -1
- package/web/.svelte-kit/output/client/.vite/manifest.json +127 -137
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BhRSsUCh.js +2 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BiLeM2i1.js +1 -0
- package/{dist/web/_app/immutable/chunks/CME08kGM.js → web/.svelte-kit/output/client/_app/immutable/chunks/BmBj85Ll.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BrERcKAH.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/Bv9252RM.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/CIXNBPKi.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DISKL3GN.js +2 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DnQ3vS13.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/KsloHTKS.js +1 -0
- package/{dist/web/_app/immutable/chunks/Ck-be5J2.js → web/.svelte-kit/output/client/_app/immutable/chunks/RsHsUj-8.js} +2 -2
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{G_zz-Gou.js → wpfV79dV.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
- package/web/.svelte-kit/output/client/_app/immutable/entry/start.Di0-Jhte.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{0.CYS8iApT.js → 0.DYyUA1au.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
- package/{dist/web/_app/immutable/nodes/3.Dr0ot9sV.js → web/.svelte-kit/output/client/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
- package/web/.svelte-kit/output/client/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{5.BBGQ_i84.js → 5.DLbPVJY2.js} +1 -1
- package/web/.svelte-kit/output/client/_app/version.json +1 -1
- package/web/.svelte-kit/output/server/.vite/manifest.json +12 -10
- package/web/.svelte-kit/output/server/chunks/Icon.js +1 -1
- package/web/.svelte-kit/output/server/chunks/client.js +1 -1
- package/web/.svelte-kit/output/server/chunks/exports.js +1 -1
- package/web/.svelte-kit/output/server/chunks/index-server.js +2 -1
- package/web/.svelte-kit/output/server/chunks/internal.js +1 -1
- package/web/.svelte-kit/output/server/chunks/render-context.js +77 -0
- package/web/.svelte-kit/output/server/chunks/root.js +739 -788
- package/web/.svelte-kit/output/server/chunks/shared.js +234 -21
- package/web/.svelte-kit/output/server/index.js +126 -90
- package/web/.svelte-kit/output/server/manifest-full.js +1 -1
- package/web/.svelte-kit/output/server/manifest.js +1 -1
- package/web/.svelte-kit/output/server/nodes/0.js +1 -1
- package/web/.svelte-kit/output/server/nodes/1.js +1 -1
- package/web/.svelte-kit/output/server/nodes/2.js +1 -1
- package/web/.svelte-kit/output/server/nodes/3.js +1 -1
- package/web/.svelte-kit/output/server/nodes/4.js +1 -1
- package/web/.svelte-kit/output/server/nodes/5.js +1 -1
- package/web/.svelte-kit/output/server/remote-entry.js +245 -81
- package/web/.svelte-kit/tsconfig.json +4 -1
- package/dist/cli/propose-policy.mjs.map +0 -1
- package/dist/lite-CBxOT1y5.mjs +0 -241
- package/dist/lite-CBxOT1y5.mjs.map +0 -1
- package/dist/routing-D8rTxtaV.mjs +0 -245
- package/dist/routing-D8rTxtaV.mjs.map +0 -1
- package/dist/web/_app/immutable/chunks/B6YN0Nuq.js +0 -1
- package/dist/web/_app/immutable/chunks/BmRlVmv6.js +0 -1
- package/dist/web/_app/immutable/chunks/CK9JZLaG.js +0 -2
- package/dist/web/_app/immutable/chunks/Ck3rYNON.js +0 -1
- package/dist/web/_app/immutable/chunks/DMtIqaiV.js +0 -2
- package/dist/web/_app/immutable/chunks/DhD271EB.js +0 -1
- package/dist/web/_app/immutable/chunks/DpuLqk8d.js +0 -1
- package/dist/web/_app/immutable/chunks/DsIToJCP.js +0 -1
- package/dist/web/_app/immutable/chunks/bBmtyQMj.js +0 -1
- package/dist/web/_app/immutable/entry/app.CJmSwntr.js +0 -2
- package/dist/web/_app/immutable/entry/start.ZpUrT2ak.js +0 -1
- package/dist/web/_app/immutable/nodes/1.Bli0Hqzn.js +0 -1
- package/dist/web/_app/immutable/nodes/4.oBhvQhcA.js +0 -60
- package/dist/workspace-BJmJBfKi.mjs +0 -456
- package/dist/workspace-BJmJBfKi.mjs.map +0 -1
- package/src/cli/e2e/agents.test.ts +0 -140
- package/src/cli/e2e/basic.test.ts +0 -43
- package/src/cli/e2e/cron.test.ts +0 -132
- package/src/cli/e2e/export-lite-func.test.ts +0 -206
- package/src/cli/e2e/fallbacks.test.ts +0 -175
- package/src/cli/e2e/init.test.ts +0 -77
- package/src/cli/e2e/messages.test.ts +0 -332
- package/src/cli/e2e/propose-policy.test.ts +0 -203
- package/src/cli/e2e/requests.test.ts +0 -180
- package/src/cli/e2e/session-timeout.test.ts +0 -192
- package/src/cli/e2e/slash-new.test.ts +0 -93
- package/src/cli/e2e/subagents.test.ts +0 -106
- package/src/cli/e2e/utils.ts +0 -66
- package/src/cli/propose-policy.ts +0 -91
- package/web/.svelte-kit/output/client/_app/immutable/chunks/B6YN0Nuq.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BmRlVmv6.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/CK9JZLaG.js +0 -2
- package/web/.svelte-kit/output/client/_app/immutable/chunks/Ck3rYNON.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DMtIqaiV.js +0 -2
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DhD271EB.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DpuLqk8d.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DsIToJCP.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/bBmtyQMj.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/entry/app.CJmSwntr.js +0 -2
- package/web/.svelte-kit/output/client/_app/immutable/entry/start.ZpUrT2ak.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/1.Bli0Hqzn.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/4.oBhvQhcA.js +0 -60
- package/web/.svelte-kit/output/server/chunks/false.js +0 -4
- /package/dist/cli/{propose-policy.d.mts → manage-policies.d.mts} +0 -0
- /package/{src/cli/e2e → e2e/_helpers}/global-setup.ts +0 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
import { detectInstall } from './install-detection.js';
|
|
6
|
+
import {
|
|
7
|
+
startControlServer,
|
|
8
|
+
type ControlRequest,
|
|
9
|
+
type ControlResponse,
|
|
10
|
+
} from './supervisor-control.js';
|
|
11
|
+
import { removeSupervisorPid } from './supervisor-pid.js';
|
|
12
|
+
import { enqueuePendingReply, dequeuePendingReply } from '../daemon/pending-replies.js';
|
|
13
|
+
import type { Supervisor } from './supervisor.js';
|
|
14
|
+
|
|
15
|
+
// Grace period before a destructive action fires. The daemon-side router
|
|
16
|
+
// awaits sendControlRequest but the daemon still needs to flush the user
|
|
17
|
+
// message + ack reply through the chat log before the daemon process is
|
|
18
|
+
// killed. SIGTERM arriving mid-flush is non-fatal (the daemon's own SIGTERM
|
|
19
|
+
// handler still runs) but the grace makes the ordering deterministic.
|
|
20
|
+
const ACTION_GRACE_MS = 1000;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Wire up the supervisor control socket so the daemon can request
|
|
24
|
+
* /restart, /shutdown, and /upgrade out-of-band.
|
|
25
|
+
*/
|
|
26
|
+
export function startSupervisorControl(supervisor: Supervisor): void {
|
|
27
|
+
startControlServer({
|
|
28
|
+
restart: async (req: ControlRequest): Promise<ControlResponse> => {
|
|
29
|
+
// Enqueue BEFORE scheduling the kill, so a crash between enqueue and
|
|
30
|
+
// kill leaves the post-restart message ready to drain.
|
|
31
|
+
if (req.chatId) {
|
|
32
|
+
enqueuePendingReply({
|
|
33
|
+
chatId: req.chatId,
|
|
34
|
+
kind: 'restart-complete',
|
|
35
|
+
...(req.messageId ? { messageId: req.messageId } : {}),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
setTimeout(() => {
|
|
39
|
+
// restartAll instead of restartService('daemon'): adapters hold
|
|
40
|
+
// tRPC subscriptions to the daemon and won't reconnect on their
|
|
41
|
+
// own when the daemon process dies, so outbound replies wouldn't
|
|
42
|
+
// make it back to the chat. Bouncing the adapters forces them to
|
|
43
|
+
// re-subscribe to the new daemon.
|
|
44
|
+
void supervisor.restartAll().catch((err) => {
|
|
45
|
+
process.stderr.write(
|
|
46
|
+
`[supervisor] /restart failed: ${err instanceof Error ? err.message : String(err)}\n`
|
|
47
|
+
);
|
|
48
|
+
// Restart failed — back out the queued reply so the next successful
|
|
49
|
+
// start doesn't surface a phantom "restarted" message.
|
|
50
|
+
if (req.chatId && req.messageId) {
|
|
51
|
+
dequeuePendingReply(
|
|
52
|
+
(e) => e.kind === 'restart-complete' && e.messageId === req.messageId
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}, ACTION_GRACE_MS);
|
|
57
|
+
return { ok: true };
|
|
58
|
+
},
|
|
59
|
+
shutdown: async (): Promise<ControlResponse> => {
|
|
60
|
+
setTimeout(() => void supervisor.shutdown(0), ACTION_GRACE_MS);
|
|
61
|
+
return { ok: true };
|
|
62
|
+
},
|
|
63
|
+
upgrade: async (req: ControlRequest): Promise<ControlResponse> => {
|
|
64
|
+
const info = detectInstall();
|
|
65
|
+
if (!info.isNpmGlobal) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
error: `not installed via npm install -g (running from ${info.entryRealPath})`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const version = req.version?.trim();
|
|
72
|
+
if (!version) {
|
|
73
|
+
return { ok: false, error: 'missing target version' };
|
|
74
|
+
}
|
|
75
|
+
if (!isAcceptableVersion(version)) {
|
|
76
|
+
return { ok: false, error: `invalid version: ${version}` };
|
|
77
|
+
}
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
void runUpgrade(supervisor, version, req.chatId, req.messageId).catch((err) => {
|
|
80
|
+
process.stderr.write(
|
|
81
|
+
`[supervisor] /upgrade encountered an unexpected error: ${err instanceof Error ? err.message : String(err)}\n`
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
}, ACTION_GRACE_MS);
|
|
85
|
+
return { ok: true };
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Accept semver-ish versions (1.2.3, 1.2.3-beta.1, etc.), the literal
|
|
91
|
+
// "latest", or a dist-tag (alphanumeric + dashes). Reject anything else so a
|
|
92
|
+
// malicious client can't smuggle shell metacharacters or `--registry=...`
|
|
93
|
+
// into the npm command line.
|
|
94
|
+
export function isAcceptableVersion(version: string): boolean {
|
|
95
|
+
return /^[a-zA-Z0-9][a-zA-Z0-9._+-]{0,63}$/.test(version);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* In-place upgrade. Order:
|
|
100
|
+
* 1. `npm install -g clawmini@<version>` (services kept running so the
|
|
101
|
+
* user isn't left in the dark if the install fails).
|
|
102
|
+
* 2. Resolve the freshly-installed binary by absolute path. Bail out with
|
|
103
|
+
* an upgrade-failed reply if it's missing.
|
|
104
|
+
* 3. Enqueue the upgrade-complete reply.
|
|
105
|
+
* 4. Stop all children, spawn the replacement supervisor, exit.
|
|
106
|
+
*
|
|
107
|
+
* Any failure path enqueues an upgrade-failed reply (so the user gets visible
|
|
108
|
+
* feedback) and restarts the daemon to drain it, rather than silently exiting.
|
|
109
|
+
*/
|
|
110
|
+
export async function runUpgrade(
|
|
111
|
+
supervisor: Supervisor,
|
|
112
|
+
version: string,
|
|
113
|
+
chatId?: string,
|
|
114
|
+
messageId?: string
|
|
115
|
+
): Promise<void> {
|
|
116
|
+
const info = detectInstall();
|
|
117
|
+
if (!info.isNpmGlobal || !info.npmRootRealPath) {
|
|
118
|
+
process.stderr.write(
|
|
119
|
+
`[supervisor] /upgrade aborted: clawmini is not installed via npm install -g (running from ${info.entryRealPath}).\n`
|
|
120
|
+
);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const installErr = await runNpmInstall(version);
|
|
125
|
+
if (installErr) {
|
|
126
|
+
process.stderr.write(`[supervisor] /upgrade failed: ${installErr}\n`);
|
|
127
|
+
if (chatId) {
|
|
128
|
+
enqueuePendingReply({
|
|
129
|
+
chatId,
|
|
130
|
+
kind: 'upgrade-failed',
|
|
131
|
+
...(messageId ? { messageId } : {}),
|
|
132
|
+
requestedVersion: version,
|
|
133
|
+
reason: installErr,
|
|
134
|
+
});
|
|
135
|
+
// The daemon is still running (we never stopped it). Bounce the whole
|
|
136
|
+
// stack so the new daemon drains the failure message AND adapters
|
|
137
|
+
// re-subscribe to it.
|
|
138
|
+
await supervisor.restartAll().catch((err) => {
|
|
139
|
+
process.stderr.write(
|
|
140
|
+
`[supervisor] additionally failed to restart services to surface the failure: ${err instanceof Error ? err.message : String(err)}\n`
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// npm install reported success — confirm the binary exists where we expect
|
|
148
|
+
// before we start tearing down services.
|
|
149
|
+
const newCli = path.join(info.npmRootRealPath, 'clawmini', 'dist', 'cli', 'index.mjs');
|
|
150
|
+
if (!fs.existsSync(newCli)) {
|
|
151
|
+
const reason = `npm install reported success but ${newCli} is missing`;
|
|
152
|
+
process.stderr.write(`[supervisor] /upgrade aborted: ${reason}\n`);
|
|
153
|
+
if (chatId) {
|
|
154
|
+
enqueuePendingReply({
|
|
155
|
+
chatId,
|
|
156
|
+
kind: 'upgrade-failed',
|
|
157
|
+
...(messageId ? { messageId } : {}),
|
|
158
|
+
requestedVersion: version,
|
|
159
|
+
reason,
|
|
160
|
+
});
|
|
161
|
+
await supervisor.restartAll().catch(() => {});
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (chatId) {
|
|
167
|
+
enqueuePendingReply({
|
|
168
|
+
chatId,
|
|
169
|
+
kind: 'upgrade-complete',
|
|
170
|
+
...(messageId ? { messageId } : {}),
|
|
171
|
+
requestedVersion: version,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
process.stderr.write('[supervisor] /upgrade: stopping all services...\n');
|
|
176
|
+
await supervisor.stopAllChildren();
|
|
177
|
+
|
|
178
|
+
process.stderr.write(`[supervisor] /upgrade: relaunching ${newCli} serve --detach...\n`);
|
|
179
|
+
// Drop our pid file so the new supervisor doesn't see us as already
|
|
180
|
+
// running; it will write its own pid on startup.
|
|
181
|
+
removeSupervisorPid();
|
|
182
|
+
|
|
183
|
+
// Run the new entry point directly via the current Node binary so the
|
|
184
|
+
// spawn doesn't depend on the shell's PATH being refreshed (which it isn't
|
|
185
|
+
// for an already-running process).
|
|
186
|
+
const replacement = spawn(process.execPath, [newCli, 'serve', '--detach'], {
|
|
187
|
+
detached: true,
|
|
188
|
+
stdio: 'ignore',
|
|
189
|
+
cwd: process.cwd(),
|
|
190
|
+
env: process.env,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const spawnOk = await waitForSpawn(replacement);
|
|
194
|
+
if (!spawnOk) {
|
|
195
|
+
process.stderr.write(
|
|
196
|
+
`[supervisor] /upgrade: replacement supervisor failed to spawn — restarting in place\n`
|
|
197
|
+
);
|
|
198
|
+
if (chatId) {
|
|
199
|
+
// Roll back the optimistic complete reply and replace it with a
|
|
200
|
+
// failure entry so the user knows the upgrade landed on disk but
|
|
201
|
+
// never came back up.
|
|
202
|
+
dequeuePendingReply(
|
|
203
|
+
(e) =>
|
|
204
|
+
e.kind === 'upgrade-complete' &&
|
|
205
|
+
e.messageId === messageId &&
|
|
206
|
+
e.requestedVersion === version
|
|
207
|
+
);
|
|
208
|
+
enqueuePendingReply({
|
|
209
|
+
chatId,
|
|
210
|
+
kind: 'upgrade-failed',
|
|
211
|
+
...(messageId ? { messageId } : {}),
|
|
212
|
+
requestedVersion: version,
|
|
213
|
+
reason: 'replacement supervisor failed to spawn',
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
// Bring the whole stack back (daemon + adapters + web) so the failure
|
|
217
|
+
// message is drained AND adapters can deliver it to the chat.
|
|
218
|
+
try {
|
|
219
|
+
await supervisor.restartAll();
|
|
220
|
+
} catch (err) {
|
|
221
|
+
process.stderr.write(
|
|
222
|
+
`[supervisor] additionally failed to restart services after spawn failure: ${err instanceof Error ? err.message : String(err)}\n`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
replacement.unref();
|
|
229
|
+
process.exit(0);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function runNpmInstall(version: string): Promise<string | null> {
|
|
233
|
+
return new Promise((resolve) => {
|
|
234
|
+
// execFile-style argv (no shell), so the version argument can't be
|
|
235
|
+
// interpreted by a shell even if it slipped past isAcceptableVersion.
|
|
236
|
+
const child = spawn('npm', ['install', '-g', `clawmini@${version}`], {
|
|
237
|
+
stdio: 'inherit',
|
|
238
|
+
env: process.env,
|
|
239
|
+
});
|
|
240
|
+
child.on('exit', (code) => {
|
|
241
|
+
if (code === 0) resolve(null);
|
|
242
|
+
else resolve(`npm install -g exited with code ${code}`);
|
|
243
|
+
});
|
|
244
|
+
child.on('error', (err) => {
|
|
245
|
+
resolve(err instanceof Error ? err.message : String(err));
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function waitForSpawn(child: ReturnType<typeof spawn>, timeoutMs = 5000): Promise<boolean> {
|
|
251
|
+
return new Promise((resolve) => {
|
|
252
|
+
let settled = false;
|
|
253
|
+
const settle = (ok: boolean): void => {
|
|
254
|
+
if (settled) return;
|
|
255
|
+
settled = true;
|
|
256
|
+
resolve(ok);
|
|
257
|
+
};
|
|
258
|
+
child.once('spawn', () => settle(true));
|
|
259
|
+
child.once('error', (err) => {
|
|
260
|
+
process.stderr.write(`[supervisor] replacement spawn error: ${err.message}\n`);
|
|
261
|
+
settle(false);
|
|
262
|
+
});
|
|
263
|
+
// 'spawn' fires almost immediately on success. Cap the wait so we never
|
|
264
|
+
// hang if neither event fires.
|
|
265
|
+
setTimeout(() => settle(true), timeoutMs);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
startControlServer,
|
|
8
|
+
sendControlRequest,
|
|
9
|
+
type ControlAction,
|
|
10
|
+
type ControlHandler,
|
|
11
|
+
type ControlRequest,
|
|
12
|
+
} from './supervisor-control.js';
|
|
13
|
+
|
|
14
|
+
describe('supervisor control socket', () => {
|
|
15
|
+
const cleanup: Array<() => void> = [];
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
while (cleanup.length) {
|
|
19
|
+
try {
|
|
20
|
+
cleanup.pop()!();
|
|
21
|
+
} catch {
|
|
22
|
+
// best-effort
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function makeSocketPath(): string {
|
|
28
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'clawmini-ctl-'));
|
|
29
|
+
cleanup.push(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
30
|
+
return path.join(tmp, 'supervisor.sock');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
it('round-trips a request to the matching handler and forwards extra fields', async () => {
|
|
34
|
+
const sockPath = makeSocketPath();
|
|
35
|
+
const seen: ControlRequest[] = [];
|
|
36
|
+
const handlers: Record<ControlAction, ControlHandler> = {
|
|
37
|
+
restart: async (req) => {
|
|
38
|
+
seen.push(req);
|
|
39
|
+
return { ok: true };
|
|
40
|
+
},
|
|
41
|
+
shutdown: async () => ({ ok: true }),
|
|
42
|
+
upgrade: async () => ({ ok: true }),
|
|
43
|
+
};
|
|
44
|
+
const server = startControlServer(handlers, sockPath);
|
|
45
|
+
cleanup.push(() => server.close());
|
|
46
|
+
|
|
47
|
+
const res = await sendControlRequest(
|
|
48
|
+
{ action: 'restart', chatId: 'c1', messageId: 'm1' },
|
|
49
|
+
sockPath
|
|
50
|
+
);
|
|
51
|
+
expect(res).toEqual({ ok: true });
|
|
52
|
+
expect(seen).toEqual([{ action: 'restart', chatId: 'c1', messageId: 'm1' }]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('returns the handler error when the handler rejects', async () => {
|
|
56
|
+
const sockPath = makeSocketPath();
|
|
57
|
+
const server = startControlServer(
|
|
58
|
+
{
|
|
59
|
+
restart: async () => {
|
|
60
|
+
throw new Error('boom');
|
|
61
|
+
},
|
|
62
|
+
shutdown: async () => ({ ok: true }),
|
|
63
|
+
upgrade: async () => ({ ok: true }),
|
|
64
|
+
},
|
|
65
|
+
sockPath
|
|
66
|
+
);
|
|
67
|
+
cleanup.push(() => server.close());
|
|
68
|
+
|
|
69
|
+
const res = await sendControlRequest({ action: 'restart' }, sockPath);
|
|
70
|
+
expect(res.ok).toBe(false);
|
|
71
|
+
expect(res.error).toBe('boom');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('rejects unknown actions', async () => {
|
|
75
|
+
const sockPath = makeSocketPath();
|
|
76
|
+
const server = startControlServer(
|
|
77
|
+
{
|
|
78
|
+
restart: async () => ({ ok: true }),
|
|
79
|
+
shutdown: async () => ({ ok: true }),
|
|
80
|
+
upgrade: async () => ({ ok: true }),
|
|
81
|
+
},
|
|
82
|
+
sockPath
|
|
83
|
+
);
|
|
84
|
+
cleanup.push(() => server.close());
|
|
85
|
+
|
|
86
|
+
const res = await sendControlRequest({ action: 'unknown' as ControlAction }, sockPath);
|
|
87
|
+
expect(res.ok).toBe(false);
|
|
88
|
+
expect(res.error).toContain('unknown action');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('overwrites a stale socket file at startup', async () => {
|
|
92
|
+
const sockPath = makeSocketPath();
|
|
93
|
+
fs.writeFileSync(sockPath, ''); // pretend a stale file is left over
|
|
94
|
+
const server = startControlServer(
|
|
95
|
+
{
|
|
96
|
+
restart: async () => ({ ok: true }),
|
|
97
|
+
shutdown: async () => ({ ok: true }),
|
|
98
|
+
upgrade: async () => ({ ok: true }),
|
|
99
|
+
},
|
|
100
|
+
sockPath
|
|
101
|
+
);
|
|
102
|
+
cleanup.push(() => server.close());
|
|
103
|
+
|
|
104
|
+
const res = await sendControlRequest({ action: 'shutdown' }, sockPath);
|
|
105
|
+
expect(res).toEqual({ ok: true });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('chmods the socket to 0600 so other users on the host cannot connect', async () => {
|
|
109
|
+
const sockPath = makeSocketPath();
|
|
110
|
+
const server = startControlServer(
|
|
111
|
+
{
|
|
112
|
+
restart: async () => ({ ok: true }),
|
|
113
|
+
shutdown: async () => ({ ok: true }),
|
|
114
|
+
upgrade: async () => ({ ok: true }),
|
|
115
|
+
},
|
|
116
|
+
sockPath
|
|
117
|
+
);
|
|
118
|
+
cleanup.push(() => server.close());
|
|
119
|
+
|
|
120
|
+
// Round-trip a request to make sure the server is fully up — once we get
|
|
121
|
+
// a response, the listen() callback that does the chmod has fired.
|
|
122
|
+
const res = await sendControlRequest({ action: 'restart' }, sockPath);
|
|
123
|
+
expect(res.ok).toBe(true);
|
|
124
|
+
|
|
125
|
+
const stat = fs.statSync(sockPath);
|
|
126
|
+
// Compare mode bits, not the full mode (which includes the file type).
|
|
127
|
+
expect(stat.mode & 0o777).toBe(0o600);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import net from 'node:net';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { getClawminiDir } from '../shared/workspace.js';
|
|
6
|
+
|
|
7
|
+
export type ControlAction = 'restart' | 'shutdown' | 'upgrade';
|
|
8
|
+
|
|
9
|
+
export interface ControlRequest {
|
|
10
|
+
action: ControlAction;
|
|
11
|
+
/** Target version for `upgrade` (e.g. "1.2.3" or "latest"). */
|
|
12
|
+
version?: string;
|
|
13
|
+
/** Chat to deliver the post-action SystemMessage to. */
|
|
14
|
+
chatId?: string;
|
|
15
|
+
/** User message that triggered the action, threaded onto the reply. */
|
|
16
|
+
messageId?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ControlResponse {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Kept short on purpose: AF_UNIX sun_path is 104 bytes on macOS, 108 on
|
|
25
|
+
// Linux. A workspace nested a few directories deep + ".clawmini/super.sock"
|
|
26
|
+
// is already close to the limit; "supervisor.sock" pushed common paths over
|
|
27
|
+
// it (EINVAL from listen()).
|
|
28
|
+
export function getControlSocketPath(startDir = process.cwd()): string {
|
|
29
|
+
return path.join(getClawminiDir(startDir), 'super.sock');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type ControlHandler = (req: ControlRequest) => Promise<ControlResponse> | ControlResponse;
|
|
33
|
+
|
|
34
|
+
export function startControlServer(
|
|
35
|
+
handlers: Record<ControlAction, ControlHandler>,
|
|
36
|
+
socketPath = getControlSocketPath()
|
|
37
|
+
): net.Server {
|
|
38
|
+
if (fs.existsSync(socketPath)) {
|
|
39
|
+
try {
|
|
40
|
+
fs.unlinkSync(socketPath);
|
|
41
|
+
} catch {
|
|
42
|
+
// best-effort
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const server = net.createServer((socket) => {
|
|
47
|
+
let buf = '';
|
|
48
|
+
let handled = false;
|
|
49
|
+
|
|
50
|
+
const respond = (res: ControlResponse): void => {
|
|
51
|
+
if (handled) return;
|
|
52
|
+
handled = true;
|
|
53
|
+
socket.end(JSON.stringify(res) + '\n');
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
socket.on('data', (chunk) => {
|
|
57
|
+
if (handled) return;
|
|
58
|
+
buf += chunk.toString();
|
|
59
|
+
const idx = buf.indexOf('\n');
|
|
60
|
+
if (idx === -1) return;
|
|
61
|
+
const line = buf.slice(0, idx);
|
|
62
|
+
let req: ControlRequest;
|
|
63
|
+
try {
|
|
64
|
+
req = JSON.parse(line) as ControlRequest;
|
|
65
|
+
} catch {
|
|
66
|
+
respond({ ok: false, error: 'invalid request' });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const handler = handlers[req.action];
|
|
70
|
+
if (!handler) {
|
|
71
|
+
respond({ ok: false, error: `unknown action: ${req.action}` });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
Promise.resolve()
|
|
75
|
+
.then(() => handler(req))
|
|
76
|
+
.then(
|
|
77
|
+
(res) => respond(res),
|
|
78
|
+
(err: unknown) =>
|
|
79
|
+
respond({ ok: false, error: err instanceof Error ? err.message : String(err) })
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
socket.on('error', () => {
|
|
84
|
+
// Ignore; the client likely went away mid-write.
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
server.listen(socketPath, () => {
|
|
89
|
+
// Restrict to the owning user. The control channel can shut down or
|
|
90
|
+
// upgrade clawmini, so a process running as another local user must not
|
|
91
|
+
// be able to connect just because they can reach the .clawmini directory.
|
|
92
|
+
try {
|
|
93
|
+
fs.chmodSync(socketPath, 0o600);
|
|
94
|
+
} catch {
|
|
95
|
+
// best-effort
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return server;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function sendControlRequest(
|
|
102
|
+
req: ControlRequest,
|
|
103
|
+
socketPath = getControlSocketPath(),
|
|
104
|
+
timeoutMs = 5000
|
|
105
|
+
): Promise<ControlResponse> {
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
const socket = net.createConnection({ path: socketPath });
|
|
108
|
+
let buf = '';
|
|
109
|
+
let settled = false;
|
|
110
|
+
|
|
111
|
+
const finish = (fn: () => void): void => {
|
|
112
|
+
if (settled) return;
|
|
113
|
+
settled = true;
|
|
114
|
+
try {
|
|
115
|
+
socket.destroy();
|
|
116
|
+
} catch {
|
|
117
|
+
// best-effort
|
|
118
|
+
}
|
|
119
|
+
fn();
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const timer = setTimeout(() => {
|
|
123
|
+
finish(() => reject(new Error(`Control request timed out after ${timeoutMs}ms`)));
|
|
124
|
+
}, timeoutMs);
|
|
125
|
+
|
|
126
|
+
socket.on('connect', () => {
|
|
127
|
+
socket.write(JSON.stringify(req) + '\n');
|
|
128
|
+
});
|
|
129
|
+
socket.on('data', (chunk) => {
|
|
130
|
+
buf += chunk.toString();
|
|
131
|
+
const idx = buf.indexOf('\n');
|
|
132
|
+
if (idx !== -1) {
|
|
133
|
+
const line = buf.slice(0, idx);
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
try {
|
|
136
|
+
const res = JSON.parse(line) as ControlResponse;
|
|
137
|
+
finish(() => resolve(res));
|
|
138
|
+
} catch (err) {
|
|
139
|
+
finish(() => reject(err instanceof Error ? err : new Error(String(err))));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
socket.on('error', (err) => {
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
finish(() => reject(err));
|
|
146
|
+
});
|
|
147
|
+
socket.on('end', () => {
|
|
148
|
+
// If the server closed before sending us a full line, surface it.
|
|
149
|
+
if (!settled) {
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
finish(() => reject(new Error('Control server closed connection without responding')));
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { getClawminiDir } from '../shared/workspace.js';
|
|
5
|
+
|
|
6
|
+
export function getSupervisorPidPath(startDir = process.cwd()): string {
|
|
7
|
+
return path.join(getClawminiDir(startDir), 'supervisor.pid');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Returns the kernel-reported start time of `pid` as an opaque string, or
|
|
11
|
+
// null if the process doesn't exist. The exact format is platform-defined
|
|
12
|
+
// but stable per-host, which is all we need: we only ever compare the
|
|
13
|
+
// stored value against a fresh read on the same machine.
|
|
14
|
+
function getProcessStartTime(pid: number): string | null {
|
|
15
|
+
try {
|
|
16
|
+
const out = execFileSync('ps', ['-p', String(pid), '-o', 'lstart='], {
|
|
17
|
+
encoding: 'utf-8',
|
|
18
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
19
|
+
});
|
|
20
|
+
const trimmed = out.trim();
|
|
21
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The pid file stores `<pid>:<start-time>` so we can detect pid reuse:
|
|
28
|
+
// `kill(pid, 0)` only confirms *something* with that pid is alive. After
|
|
29
|
+
// the supervisor exits the OS may hand the same pid to an unrelated
|
|
30
|
+
// process (browser tab, ssh, etc.) — without the start-time check we'd
|
|
31
|
+
// happily SIGTERM that.
|
|
32
|
+
export function readSupervisorPid(startDir = process.cwd()): number | null {
|
|
33
|
+
const p = getSupervisorPidPath(startDir);
|
|
34
|
+
if (!fs.existsSync(p)) return null;
|
|
35
|
+
const content = fs.readFileSync(p, 'utf-8').trim();
|
|
36
|
+
const sep = content.indexOf(':');
|
|
37
|
+
if (sep <= 0) return null;
|
|
38
|
+
const pid = parseInt(content.slice(0, sep), 10);
|
|
39
|
+
const storedStart = content.slice(sep + 1).trim();
|
|
40
|
+
if (!Number.isFinite(pid) || pid <= 0 || storedStart.length === 0) return null;
|
|
41
|
+
try {
|
|
42
|
+
process.kill(pid, 0);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const currentStart = getProcessStartTime(pid);
|
|
47
|
+
if (currentStart === null || currentStart !== storedStart) return null;
|
|
48
|
+
return pid;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function writeSupervisorPid(pid: number, startDir = process.cwd()): void {
|
|
52
|
+
const startTime = getProcessStartTime(pid);
|
|
53
|
+
if (!startTime) {
|
|
54
|
+
throw new Error(`Cannot read start time for pid ${pid}; refusing to write supervisor.pid`);
|
|
55
|
+
}
|
|
56
|
+
fs.writeFileSync(getSupervisorPidPath(startDir), `${pid}:${startTime}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function removeSupervisorPid(startDir = process.cwd()): void {
|
|
60
|
+
const p = getSupervisorPidPath(startDir);
|
|
61
|
+
if (fs.existsSync(p)) {
|
|
62
|
+
try {
|
|
63
|
+
fs.unlinkSync(p);
|
|
64
|
+
} catch {
|
|
65
|
+
// best-effort cleanup
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|