elanous 0.2.21 → 0.2.22
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/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/1307-08b716476f1d50af.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2489-ff55a35b202c0f94.js → 2489-d238d3e2eb5b2bf5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3857-5985236d9307f676.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5633-d6852385000738ef.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-5b9e8a45a5b0c823.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8857-aa1cc4ac90af4d40.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-55bcfbb0e8057f59.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-88459e0903da2803.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-d781be3e3a9cfd74.js → page-d85c43f46f58716b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-b0a80997903852d8.js → page-4e645600579ae952.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-2b1263165630d9c8.js → page-87edbfd19117823b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-7f0c2b7801ac6a54.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-bf1f030a9d1adffd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-e2d6bed48ac20120.js → page-cbaf66b1e1804adf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-111cabe4d1fd69ae.js → page-80ed511e02c81412.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/{page-544dd4bd4847eeb7.js → page-9f5610caaa06178c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/{page-3ee191319bf97d87.js → page-ec8f3575e339de74.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-86d753e460f1a989.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-4166a7d51cf0a57d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-a449f1ed472b81c2.js → page-2a19c5cd3b562580.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-9045094d6d3b5cd0.js → page-b6a09cde2e07521a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-c06673596849ef45.js → page-f7eae1518ca0c353.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-456a8573ad811381.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/{layout-9f81233f29dcd409.js → layout-8462d6a543cbb2d9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-2974cdfb87c0f9c9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live-run/page-f927ffa25f3c86d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-abd86cd832aaa011.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-79daf68f6256adaa.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8c3f168fc3f91939.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-7a12f61be4a579d1.js → page-786fb4504d0f2fa9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-c85b0589591a99c9.js → not-found-75209f14597dcb8f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-725a4da02dd129eb.js → page-bf30affe3a85af7e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1187a44b7515434b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-3d0910185a66ed54.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-f401ee1dd5a26e25.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-5dfe7604fd842784.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/{page-21bec4afeb2f0917.js → page-76cf838050a8d681.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-dc39ee7a36d26385.js → page-7ec8e289b4c14df6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-84ae759f76053672.js → page-8c8ac17d52be2a79.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-3db58ddb61fe2be9.js → page-d359ab048d63a9d9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-c5315e30f3528dc4.js → page-b8b345c880bf78d1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-2fe368efb6de3b6a.js → page-f151cc10ad026940.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-9875f103057da433.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-9789328af9a9aa62.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-c69762987b751174.js → page-bf495a1e284f4e00.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a263d94bca4be22a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-3962eab8ec58d16e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-9abcfac07e02a542.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-bee49f927c333abb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-bac2d56dcb02d955.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-937cbcc13176295c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d09495594f7f480d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-7f8d7fcdf058a845.js → page-69cb93d1a2e57e0e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-22163556cd38bc88.js → page-72ec543928d67c3e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-82906eeda0186a7b.js +1 -0
- package/apps/pwa/out/_next/static/css/f2bd5257e6af88d8.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/live-run/index.html +1 -1
- package/apps/pwa/out/live-run/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/graphs/catalog/catalog.yaml +1 -1
- package/graphs/drafts/recipes.yaml +2 -2
- package/graphs/test-diet/recipes.yaml +3 -0
- package/graphs/test-diet/test-diet.yaml +3 -1
- package/package.json +1 -1
- package/scripts/f-session-monitor.ts +69 -0
- package/scripts/hq/standby-snapshot.ts +7 -3
- package/scripts/knowledge-compress.ts +11 -0
- package/scripts/nightly-docops.ts +90 -3
- package/scripts/research-coo-admin-dates.ts +39 -0
- package/scripts/skills/public-check.ts +7 -2
- package/scripts/smart-merge/resync-shadow.ts +5 -3
- package/scripts/test-diet/lib.ts +81 -1
- package/scripts/test-diet/node.ts +48 -2
- package/scripts/verify-coo-admin-evidence.ts +83 -0
- package/src/acp/client.ts +4 -2
- package/src/agent/run-context.ts +4 -0
- package/src/agent-env/profile.ts +25 -5
- package/src/agent-mission/driver.ts +26 -12
- package/src/agent-mission/supervisor-metrics.ts +17 -2
- package/src/agent-substrate/acp-reviewer.ts +12 -5
- package/src/agent-substrate/pr-reviewer.ts +11 -4
- package/src/agent-substrate/review-intent.ts +102 -3
- package/src/autopilot/build/llm-conflict-merge.ts +22 -8
- package/src/autopilot/pr-manager.ts +18 -8
- package/src/away/release-watch.ts +47 -8
- package/src/briefing/brief-items.ts +104 -11
- package/src/chat/index.ts +1 -0
- package/src/cli/brief-cli.ts +20 -2
- package/src/cli/config-classify.ts +83 -0
- package/src/cli/control-cli.ts +14 -0
- package/src/cli/decisions-cli.ts +26 -16
- package/src/cli/logs-model-input.ts +1 -1
- package/src/cli/pr-cli.ts +65 -8
- package/src/cli/release-cli.ts +21 -4
- package/src/cli/tasks-cli.ts +108 -1
- package/src/connectors/types.ts +6 -6
- package/src/context-bus/context-now-surfaces.ts +18 -0
- package/src/context-bus/coord-events.ts +32 -4
- package/src/context-bus/long-term-memory.ts +23 -6
- package/src/context-bus/nightly-condense.ts +169 -21
- package/src/control/control-status.ts +87 -0
- package/src/control-plane/light-member.ts +5 -3
- package/src/control-plane/member.ts +172 -7
- package/src/core-turn/run-goal-loop.ts +22 -1
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +9 -0
- package/src/decisions/decision-cards.ts +20 -1
- package/src/decisions/decision-ledger.ts +11 -8
- package/src/decisions/web-push-decision-cards.ts +19 -3
- package/src/discord-self-message.ts +2 -2
- package/src/domains/coo-admin-dates.evidence.json +876 -0
- package/src/domains/coo-admin-tool.ts +48 -14
- package/src/domains/ops-status.ts +53 -0
- package/src/domains/outbound-alert.ts +24 -1
- package/src/exec-requests/planner.ts +6 -0
- package/src/graph-kinds/registry.ts +6 -6
- package/src/graph-runner/graph-cli.ts +34 -2
- package/src/graph-runner/graph-run-api.ts +7 -0
- package/src/graph-runner/graph-tick.ts +73 -12
- package/src/graph-runner/runner.ts +43 -11
- package/src/graph-unify/wf-to-graph.ts +79 -0
- package/src/graph-wizard/generate.ts +34 -2
- package/src/graph-wizard/steps.ts +13 -0
- package/src/harness/harness-cli-command.ts +121 -23
- package/src/harness/harness-queue.ts +58 -14
- package/src/harness/launch-advice.ts +23 -0
- package/src/index.ts +36 -7
- package/src/knowledge/conclusion-compress.ts +93 -0
- package/src/knowledge/know-find.ts +9 -2
- package/src/knowledge/query.ts +20 -0
- package/src/llm.ts +26 -16
- package/src/loops/neighbor-runtime.ts +88 -0
- package/src/loops/orchestrator/author-depth.ts +29 -11
- package/src/loops/orchestrator/overlap.ts +7 -1
- package/src/loops/orchestrator/resync-shadow.ts +1 -1
- package/src/loops/orchestrator/tick.ts +35 -2
- package/src/loops/orchestrator/traffic.ts +2 -0
- package/src/loops/registry.ts +5 -0
- package/src/market/publish.ts +17 -1
- package/src/maturity/feature-maturity.ts +2 -1
- package/src/mss/logging/redaction.ts +17 -0
- package/src/nexus/api/draft-metrics.ts +22 -8
- package/src/nexus/api/graph-peer-edit.ts +77 -0
- package/src/nexus/api/graph-wizard-api.ts +9 -1
- package/src/nexus/api/graphs-api.ts +145 -7
- package/src/nexus/api/http-server.ts +93 -8
- package/src/nexus/api/plugins-market.ts +24 -0
- package/src/nexus/index.ts +3 -2
- package/src/oauth/claude-pty-maturity.ts +24 -0
- package/src/oauth/fallback-chain.ts +27 -14
- package/src/plugins/maker/market-bundle.ts +9 -0
- package/src/plugins/maker/plugin-maker.ts +9 -3
- package/src/plugins/maker/wizard-generate.ts +73 -9
- package/src/policy/llm-policy.ts +11 -6
- package/src/release-loop/checklist-board.ts +27 -0
- package/src/release-loop/checklist.ts +38 -0
- package/src/seat-address/seat-address.ts +1 -0
- package/src/seat-loop/seat-loop.ts +174 -20
- package/src/seat-loop/seat-report.ts +29 -1
- package/src/self-dev/dev-cli.ts +90 -8
- package/src/self-dev/dev-pipeline.ts +20 -1
- package/src/self-dev/draft-sweep.ts +45 -8
- package/src/self-dev/orchestrate.ts +3 -0
- package/src/self-dev/review-provider-fallback.ts +83 -0
- package/src/self-dev/run-supervisor.ts +5 -1
- package/src/self-implement/harness-policy-gates.ts +7 -5
- package/src/self-implement/host-regate.ts +28 -14
- package/src/self-implement/l8-merge-queue.ts +80 -8
- package/src/self-implement/orchestrator.ts +68 -27
- package/src/self-implement/seams.ts +5 -3
- package/src/self-implement/self-implement-cli.ts +4 -0
- package/src/shell-env-bootstrap.ts +19 -4
- package/src/steward/triage.ts +25 -8
- package/src/storage/file-lock.ts +33 -4
- package/src/task-agent/completion-evidence.ts +31 -3
- package/src/task-agent/cover.ts +61 -0
- package/src/task-agent/live-moves.ts +126 -3
- package/src/task-agent/shadow.ts +19 -4
- package/src/task-agent/task-hand.ts +2 -0
- package/src/task-orchestrator/surfaces/pod-pool.ts +138 -15
- package/src/task-orchestrator/surfaces/self-implement.ts +7 -0
- package/src/telegram-commands.ts +8 -0
- package/src/telegram-project-command.ts +6 -0
- package/src/telegram.ts +2 -2
- package/src/typecheck-ratchet.ts +2 -1
- package/src/user-config.ts +35 -4
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/executor.ts +24 -2
- package/src/workflow-runtime/triggers/registry.ts +27 -0
- package/src/workflow-runtime/triggers/schedule-source.ts +30 -3
- package/apps/pwa/out/_next/static/chunks/1307-c97eba9fb72a9a5e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-59fedea50d13c762.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8857-f1762140ef9a96b1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9247-7581825d511a64c0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9497-b0e758d1cd4ae8d7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-937c92292a4ab4f0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-ea998f7f5d156bea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-33c328b64ca508f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-e4bcb2c503dab04b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-300897dcbbd5ed76.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-103fa3191c1b2e08.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-be8b4615c919e36c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live-run/page-c685b104e0dfa609.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-8d693331cfbb6add.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-cef4f30a4c22fae1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66ea1b54fc83fa15.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-8cc707bd09a497d9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-90b55be1f15ce0e8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cb16606e08be65db.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-16dc64b74ed6a40a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-8d04132dd0ac5e10.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-f23484de467c9ea3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-5456692bda39aafb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-d75f7cd65845ce98.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-5603f5e568b25ce3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-d2cc0f4e89f42028.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-a6eb73b8a4840f1c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c0a3f4d10edb83fa.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6c0b88068c143f15.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-141436fd26dd56fc.js +0 -1
- package/apps/pwa/out/_next/static/css/e81df11490b4413b.css +0 -1
- /package/apps/pwa/out/_next/static/{MObU8ujkMciAKr6ljAV05 → aBi6EyMtLsrl7J8eH4kF7}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{MObU8ujkMciAKr6ljAV05 → aBi6EyMtLsrl7J8eH4kF7}/_ssgManifest.js +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
4
5
|
import { parse as parseYaml } from 'yaml';
|
|
5
6
|
import { elanousStateRoot } from '../../autopilot/state-paths.js';
|
|
6
7
|
import { debug } from '../../debug/log.js';
|
|
@@ -10,6 +11,7 @@ import { validateGraphYaml } from './graph-kinds.js';
|
|
|
10
11
|
import { parseWizardSteps, readWizardSteps, withWizardSteps } from '../../graph-wizard/saved-steps.js';
|
|
11
12
|
import type { WizardNodeStep } from '../../graph-wizard/steps.js';
|
|
12
13
|
import { jsonResponse } from './json-response.js';
|
|
14
|
+
import { clearPeerEdit, peerEditRefusalBody, peerEditRunGate, readPeerEdit, writePeerEdit } from './graph-peer-edit.js';
|
|
13
15
|
|
|
14
16
|
/** Declared graph ids only. A user string is never joined onto a filesystem path. */
|
|
15
17
|
const GRAPH_ID = /^[a-z0-9-]+$/;
|
|
@@ -44,6 +46,93 @@ function mineGraphsDir(): string {
|
|
|
44
46
|
return join(elanousStateRoot(), 'graphs');
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
export type GraphPermission = 'view' | 'edit';
|
|
50
|
+
type GraphGrant = { recipient: string; permission: GraphPermission };
|
|
51
|
+
const TOKEN_PATTERN = /^eg_[A-Za-z0-9_-]{32,}$/;
|
|
52
|
+
|
|
53
|
+
function accessFile(dir: string, id: string): string {
|
|
54
|
+
return join(dir, '.access', `${id}.json`);
|
|
55
|
+
}
|
|
56
|
+
function tokenHash(token: string): string {
|
|
57
|
+
return createHash('sha256').update(token).digest('hex');
|
|
58
|
+
}
|
|
59
|
+
function accessGrants(dir: string, id: string): GraphGrant[] {
|
|
60
|
+
let text: string;
|
|
61
|
+
try { text = readFileSync(accessFile(dir, id), 'utf8'); }
|
|
62
|
+
catch (error) {
|
|
63
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
const parsed: unknown = JSON.parse(text);
|
|
67
|
+
if (!Array.isArray(parsed) || !parsed.every((entry): entry is GraphGrant => entry !== null && typeof entry === 'object'
|
|
68
|
+
&& typeof entry.recipient === 'string' && /^[a-f0-9]{64}$/.test(entry.recipient)
|
|
69
|
+
&& (entry.permission === 'view' || entry.permission === 'edit'))) throw new Error('invalid graph access store');
|
|
70
|
+
return parsed;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Only a hash is persisted; no owner bearer or raw recipient credential goes into a graph/YAML. */
|
|
74
|
+
export function handleGraphAccess(pathname: string, req: Request, deps: GraphsApiDeps = {}): Response {
|
|
75
|
+
const match = /^\/v1\/graphs\/([^/]+)\/access$/.exec(pathname);
|
|
76
|
+
const id = match ? resolveId(match[1]!) : null;
|
|
77
|
+
if (!id) return jsonResponse({ error: 'bad_request' }, 400);
|
|
78
|
+
const owned = mineOwnership(id, deps);
|
|
79
|
+
if (owned instanceof Response) return owned;
|
|
80
|
+
if (req.method !== 'GET') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
81
|
+
try { return jsonResponse({ grants: accessGrants(owned.mineDir, id) }); }
|
|
82
|
+
catch (error) { return saveFailed(id, error); }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function handleGraphAccessPut(pathname: string, req: Request, deps: GraphsApiDeps = {}): Promise<Response> {
|
|
86
|
+
const match = /^\/v1\/graphs\/([^/]+)\/access$/.exec(pathname);
|
|
87
|
+
const id = match ? resolveId(match[1]!) : null;
|
|
88
|
+
if (!id) return jsonResponse({ error: 'bad_request' }, 400);
|
|
89
|
+
const owned = mineOwnership(id, deps);
|
|
90
|
+
if (owned instanceof Response) return owned;
|
|
91
|
+
// Detail GET resolves a core id first; do not grant a token for a shadowed mine id.
|
|
92
|
+
if (indexByDeclaredId(deps.coreDir ?? defaultGraphsDir()).has(id)) return jsonResponse({ error: 'core-id-conflict', id }, 409);
|
|
93
|
+
let body: { recipient?: unknown; permission?: unknown };
|
|
94
|
+
try { body = await req.json() as typeof body; }
|
|
95
|
+
catch { return jsonResponse({ error: 'bad_request' }, 400); }
|
|
96
|
+
if (!body || typeof body.recipient !== 'string' || !TOKEN_PATTERN.test(body.recipient)
|
|
97
|
+
|| (body.permission !== 'view' && body.permission !== 'edit')) {
|
|
98
|
+
return jsonResponse({ error: 'bad_request', reason: 'recipient access token and view/edit permission required' }, 400);
|
|
99
|
+
}
|
|
100
|
+
const recipient = tokenHash(body.recipient);
|
|
101
|
+
const dir = join(owned.mineDir, '.access');
|
|
102
|
+
try {
|
|
103
|
+
const grants = accessGrants(owned.mineDir, id).filter((grant) => grant.recipient !== recipient);
|
|
104
|
+
grants.push({ recipient, permission: body.permission });
|
|
105
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
106
|
+
const target = accessFile(owned.mineDir, id);
|
|
107
|
+
const temp = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
108
|
+
try {
|
|
109
|
+
writeFileSync(temp, JSON.stringify(grants), { mode: 0o600 });
|
|
110
|
+
renameSync(temp, target);
|
|
111
|
+
} finally { rmSync(temp, { force: true }); }
|
|
112
|
+
} catch (error) { return saveFailed(id, error); }
|
|
113
|
+
return jsonResponse({ recipient, permission: body.permission });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The peer token works only on this graph and the requested read/edit operation. */
|
|
117
|
+
export function graphAccessForRequest(pathname: string, method: string, token: string | null, deps: GraphsApiDeps = {}): boolean {
|
|
118
|
+
const match = /^\/v1\/graphs\/([^/]+)(?:\/(yaml))?$/.exec(pathname);
|
|
119
|
+
const id = match ? resolveId(match[1]!) : null;
|
|
120
|
+
if (!id || !token || !TOKEN_PATTERN.test(token)) return false;
|
|
121
|
+
if (method !== 'GET' && !(method === 'PUT' && match?.[2] === 'yaml')) return false;
|
|
122
|
+
try {
|
|
123
|
+
if (!indexByDeclaredId(deps.mineDir ?? mineGraphsDir()).has(id)) return false;
|
|
124
|
+
if (indexByDeclaredId(deps.coreDir ?? defaultGraphsDir()).has(id)) return false;
|
|
125
|
+
const wanted = tokenHash(token);
|
|
126
|
+
return accessGrants(deps.mineDir ?? mineGraphsDir(), id).some((grant) => {
|
|
127
|
+
const stored = Buffer.from(grant.recipient, 'hex');
|
|
128
|
+
return timingSafeEqual(stored, Buffer.from(wanted, 'hex')) && (method === 'GET' || grant.permission === 'edit');
|
|
129
|
+
});
|
|
130
|
+
} catch (error) {
|
|
131
|
+
debug.log('nexus.graphs', 'access-read-failed', { id, reason: String(error) }, { level: 'warn' });
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
47
136
|
function resolveId(raw: string): string | null {
|
|
48
137
|
let id: string;
|
|
49
138
|
try { id = decodeURIComponent(raw); }
|
|
@@ -203,7 +292,10 @@ export function handleGraphsGet(pathname: string, dir = defaultGraphsDir(), deps
|
|
|
203
292
|
}
|
|
204
293
|
|
|
205
294
|
/** PUT writes only under the state-root graphs directory. Core YAML is never opened for write. */
|
|
206
|
-
|
|
295
|
+
/** Who authorized a save: the owner, or a peer token holding an «edit» grant (raw token; only a hash prefix is kept). */
|
|
296
|
+
export type GraphSaveActor = { peerToken: string } | null;
|
|
297
|
+
|
|
298
|
+
export async function handleGraphsPut(pathname: string, req: Request, deps: GraphsApiDeps = {}, actor: GraphSaveActor = null): Promise<Response> {
|
|
207
299
|
const match = /^\/v1\/graphs\/([^/]+)\/yaml$/.exec(pathname);
|
|
208
300
|
if (!match) return jsonResponse({ error: 'not-found' }, 404);
|
|
209
301
|
const id = resolveId(match[1]!);
|
|
@@ -229,11 +321,21 @@ export async function handleGraphsPut(pathname: string, req: Request, deps: Grap
|
|
|
229
321
|
// Writes go only to <mine>/<id>.yaml; a hand-placed file under another name is not edited over HTTP.
|
|
230
322
|
if (existing.file !== `${id}.yaml`) return jsonResponse({ error: 'file-name-mismatch', id, file: existing.file }, 409);
|
|
231
323
|
let written: { version: string; previous: string };
|
|
232
|
-
|
|
324
|
+
const editedBy = actor ? `peer:${tokenHash(actor.peerToken).slice(0, 8)}` : null;
|
|
325
|
+
try {
|
|
326
|
+
written = withWizardSteps(mineDir, id, steps, () => {
|
|
327
|
+
// Peer save: the marker goes down «before» the bytes, so a crash in between still refuses the run.
|
|
328
|
+
if (editedBy) writePeerEdit(mineDir, id, { editedBy, version: 'pending', at: new Date().toISOString() });
|
|
329
|
+
const result = writeWithVersion(mineDir, id, existing, body.yaml as string);
|
|
330
|
+
if (editedBy) writePeerEdit(mineDir, id, { editedBy, version: result.version, at: new Date().toISOString() });
|
|
331
|
+
else clearPeerEdit(mineDir, id);
|
|
332
|
+
return result;
|
|
333
|
+
});
|
|
334
|
+
}
|
|
233
335
|
catch (error) { return saveFailed(id, error); }
|
|
234
336
|
const { version, previous } = written;
|
|
235
|
-
debug.log('nexus.graphs', 'saved', { id, version, previous, steps: steps ? Object.keys(steps).length : 0 });
|
|
236
|
-
return jsonResponse({ id, source: 'mine', editable: true, saved: true, version, previous });
|
|
337
|
+
debug.log('nexus.graphs', 'saved', { id, version, previous, steps: steps ? Object.keys(steps).length : 0, ...(editedBy ? { editedBy } : {}) });
|
|
338
|
+
return jsonResponse({ id, source: 'mine', editable: true, saved: true, version, previous, ...(editedBy ? { editedBy } : {}) });
|
|
237
339
|
}
|
|
238
340
|
|
|
239
341
|
const VERSION_ID = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z(?:-\d{4})?$/;
|
|
@@ -408,8 +510,14 @@ export async function handleGraphsClone(pathname: string, req: Request, deps: Gr
|
|
|
408
510
|
if (reservedId) return reservedId;
|
|
409
511
|
const coreDir = deps.coreDir ?? defaultGraphsDir();
|
|
410
512
|
const mineDir = deps.mineDir ?? mineGraphsDir();
|
|
411
|
-
const
|
|
513
|
+
const fromCore = indexByDeclaredId(coreDir).get(id);
|
|
514
|
+
const source = fromCore ?? indexByDeclaredId(mineDir).get(id);
|
|
412
515
|
if (!source) return jsonResponse({ error: 'not-found', id }, 404);
|
|
516
|
+
// A clone would carry a peer's unapproved change into a graph with no marker — approve first.
|
|
517
|
+
if (!fromCore) {
|
|
518
|
+
const gate = peerEditRunGate(mineDir, id);
|
|
519
|
+
if (!gate.ok) return jsonResponse(peerEditRefusalBody(id, gate), 409);
|
|
520
|
+
}
|
|
413
521
|
if (indexByDeclaredId(mineDir).has(body.newId) || indexByDeclaredId(coreDir).has(body.newId)) {
|
|
414
522
|
return jsonResponse({ error: 'conflict', id: body.newId }, 409);
|
|
415
523
|
}
|
|
@@ -422,9 +530,39 @@ export async function handleGraphsClone(pathname: string, req: Request, deps: Gr
|
|
|
422
530
|
return jsonResponse({ id: body.newId, source: 'mine', editable: true, clonedFrom: id }, 201);
|
|
423
531
|
}
|
|
424
532
|
|
|
425
|
-
|
|
533
|
+
/** POST /v1/graphs/<id>/approve {version} — owner only (the route never accepts a peer token). Approves the
|
|
534
|
+
* exact peer version the run refusal named; a newer peer save is a new version and needs a new approval. */
|
|
535
|
+
export async function handleGraphsApprove(pathname: string, req: Request, deps: GraphsApiDeps = {}): Promise<Response> {
|
|
536
|
+
const match = /^\/v1\/graphs\/([^/]+)\/approve$/.exec(pathname);
|
|
537
|
+
if (!match) return jsonResponse({ error: 'not-found' }, 404);
|
|
538
|
+
const id = resolveId(match[1]!);
|
|
539
|
+
if (!id) return jsonResponse({ error: 'bad_request', reason: 'invalid graph id' }, 400);
|
|
540
|
+
const owned = mineOwnership(id, deps);
|
|
541
|
+
if (owned instanceof Response) return owned;
|
|
542
|
+
let body: { version?: unknown };
|
|
543
|
+
try { body = await req.json() as { version?: unknown }; }
|
|
544
|
+
catch { return jsonResponse({ error: 'bad_request', reason: 'json body required' }, 400); }
|
|
545
|
+
if (typeof body?.version !== 'string' || !body.version) return jsonResponse({ error: 'bad_request', reason: 'version string required' }, 400);
|
|
546
|
+
let record;
|
|
547
|
+
try { record = readPeerEdit(owned.mineDir, id); }
|
|
548
|
+
catch (error) { return saveFailed(id, error); }
|
|
549
|
+
if (!record) return jsonResponse({ id, approved: true, version: body.version, pending: false });
|
|
550
|
+
if (record.version !== body.version) {
|
|
551
|
+
return jsonResponse({ error: 'version-mismatch', id, version: record.version, reason: '승인하려는 판이 최신 상대 변경과 다르다 — 다시 확인하라' }, 409);
|
|
552
|
+
}
|
|
553
|
+
const approved = { version: record.version, at: new Date().toISOString() };
|
|
554
|
+
try { writePeerEdit(owned.mineDir, id, { ...record, approved }); }
|
|
555
|
+
catch (error) { return saveFailed(id, error); }
|
|
556
|
+
debug.log('nexus.graphs', 'peer-edit-approved', { id, version: record.version, editedBy: record.editedBy });
|
|
557
|
+
return jsonResponse({ id, approved: true, version: record.version, editedBy: record.editedBy });
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export async function handleGraphsMutation(pathname: string, req: Request, deps: GraphsApiDeps = {}, actor: GraphSaveActor = null): Promise<Response | null> {
|
|
561
|
+
// Only PUT …/yaml is open to a peer token; every other mutation is the owner's alone.
|
|
562
|
+
if (actor && !(req.method === 'PUT' && /^\/v1\/graphs\/[^/]+\/yaml$/.test(pathname))) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
426
563
|
if (req.method === 'POST' && pathname === '/v1/graphs') return handleGraphsCreate(req, deps);
|
|
427
|
-
if (req.method === 'PUT' && /^\/v1\/graphs\/[^/]+\/yaml$/.test(pathname)) return handleGraphsPut(pathname, req, deps);
|
|
564
|
+
if (req.method === 'PUT' && /^\/v1\/graphs\/[^/]+\/yaml$/.test(pathname)) return handleGraphsPut(pathname, req, deps, actor);
|
|
565
|
+
if (req.method === 'POST' && /^\/v1\/graphs\/[^/]+\/approve$/.test(pathname)) return handleGraphsApprove(pathname, req, deps);
|
|
428
566
|
if (req.method === 'POST' && /^\/v1\/graphs\/[^/]+\/clone$/.test(pathname)) return handleGraphsClone(pathname, req, deps);
|
|
429
567
|
if (req.method === 'POST' && /^\/v1\/graphs\/[^/]+\/revert$/.test(pathname)) return handleGraphsRevert(pathname, req, deps);
|
|
430
568
|
return null;
|
|
@@ -19,7 +19,9 @@ import { setInProcessOutbound } from '../../domains/outbound-alert.js';
|
|
|
19
19
|
import { getUserConfig } from '../../user-config.js';
|
|
20
20
|
import type { TelegramEvent } from '../../workflow-runtime/triggers/telegram-source.js';
|
|
21
21
|
import { spawnSync } from 'node:child_process';
|
|
22
|
-
import { isAbsolute } from 'node:path';
|
|
22
|
+
import { isAbsolute, join, dirname, relative } from 'node:path';
|
|
23
|
+
import { homedir } from 'node:os';
|
|
24
|
+
import { readdirSync, realpathSync, statSync } from 'node:fs';
|
|
23
25
|
import { createProject, listProjects } from '../../project/project-store.js';
|
|
24
26
|
import { handleOutputsGet, type OutputsDeps } from './outputs-route.js';
|
|
25
27
|
import { buildGridData, type GridDeps } from './grid.js';
|
|
@@ -368,12 +370,12 @@ import {
|
|
|
368
370
|
} from './workflows.js';
|
|
369
371
|
import { handleTriggersSnapshot } from './triggers.js';
|
|
370
372
|
import { handleWorkflowTemplatesList } from './workflow-templates.js';
|
|
371
|
-
import { handleGraphVersionsGet, handleGraphsGet, handleGraphsMutation } from './graphs-api.js';
|
|
373
|
+
import { graphAccessForRequest, handleGraphAccess, handleGraphAccessPut, handleGraphVersionsGet, handleGraphsGet, handleGraphsMutation } from './graphs-api.js';
|
|
372
374
|
import { handleGraphKindsGet, handleGraphsValidatePost } from './graph-kinds.js';
|
|
373
|
-
import { handleGraphWizardPost } from './graph-wizard-api.js';
|
|
375
|
+
import { handleGraphWizardPost, handleGraphWizardPacksGet } from './graph-wizard-api.js';
|
|
374
376
|
import { syncInstalledPluginNodes } from '../../graph-kinds/installed-plugin-nodes.js';
|
|
375
377
|
import { elanousStateRoot } from '../../autopilot/state-paths.js';
|
|
376
|
-
import { handlePluginsIndexGet, handlePluginsGet, handlePluginsMarketRefresh, handlePluginsInstall, handlePluginsRemove } from './plugins-market.js';
|
|
378
|
+
import { handlePluginsIndexGet, handlePluginsGet, handlePluginsMarketRefresh, handlePluginsInstall, handlePluginsRemove, handlePluginWizardList, handlePluginWizardSave } from './plugins-market.js';
|
|
377
379
|
import { handlePluginsCredentialsGet, handlePluginsCredentialsPut } from './plugins-credentials-api.js';
|
|
378
380
|
import {
|
|
379
381
|
handleWorkflowsList,
|
|
@@ -489,6 +491,8 @@ export interface NexusHttpServerOpts {
|
|
|
489
491
|
fabricPlans?: FabricPlansRouteOpts;
|
|
490
492
|
/** GRAPH-WIZARD 시험 주입(LLM·템플릿 뿌리). 운영은 비워 둔다. */
|
|
491
493
|
graphWizard?: import('../../graph-wizard/generate.js').GraphWizardDeps;
|
|
494
|
+
/** Roots the remote folder picker may browse (default: the daemon user's home). Tests inject a temp root. */
|
|
495
|
+
projectFolderRoots?: string[];
|
|
492
496
|
/** Optional installer state root for isolated API consumers and tests. */
|
|
493
497
|
pluginStateRoot?: string;
|
|
494
498
|
outputs?: OutputsDeps;
|
|
@@ -846,6 +850,55 @@ export function handleProjectsGet(): Response {
|
|
|
846
850
|
return jsonResponse({ projects: listProjects() });
|
|
847
851
|
}
|
|
848
852
|
|
|
853
|
+
function isWithinRoot(path: string, root: string): boolean {
|
|
854
|
+
const rel = relative(root, path);
|
|
855
|
+
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Remote folder picker: lists sub-folders under an allow-listed root only.
|
|
860
|
+
* Every path is resolved with realpath first, so `..` segments and symlinks cannot escape the roots;
|
|
861
|
+
* dot-folders are hidden and symlinked entries that point outside the roots are not listed.
|
|
862
|
+
*/
|
|
863
|
+
export function handleProjectFoldersGet(req: Request, roots: string[] = [homedir()]): Response {
|
|
864
|
+
const allowed: string[] = [];
|
|
865
|
+
for (const root of roots) {
|
|
866
|
+
try { allowed.push(realpathSync(root)); } catch { /* a missing root grants nothing */ }
|
|
867
|
+
}
|
|
868
|
+
if (allowed.length === 0) return jsonResponse({ error: 'folder_unavailable' }, 404);
|
|
869
|
+
const requested = new URL(req.url).searchParams.get('path') ?? allowed[0]!;
|
|
870
|
+
if (!isAbsolute(requested) || requested.includes('\0')) return jsonResponse({ error: 'invalid_folder' }, 400);
|
|
871
|
+
let path: string;
|
|
872
|
+
try { path = realpathSync(requested); }
|
|
873
|
+
catch {
|
|
874
|
+
debug.log('nexus.projects', 'folders-unavailable', { reason: 'missing' });
|
|
875
|
+
return jsonResponse({ error: 'folder_unavailable' }, 404);
|
|
876
|
+
}
|
|
877
|
+
if (!allowed.some(root => isWithinRoot(path, root))) {
|
|
878
|
+
debug.log('nexus.projects', 'folders-refused', { reason: 'outside-roots' });
|
|
879
|
+
return jsonResponse({ error: 'outside_allowed_roots' }, 403);
|
|
880
|
+
}
|
|
881
|
+
try {
|
|
882
|
+
if (!statSync(path).isDirectory()) return jsonResponse({ error: 'not_directory' }, 400);
|
|
883
|
+
const folders = readdirSync(path, { withFileTypes: true })
|
|
884
|
+
.filter(entry => {
|
|
885
|
+
if (entry.name.startsWith('.')) return false;
|
|
886
|
+
if (entry.isDirectory()) return true;
|
|
887
|
+
if (!entry.isSymbolicLink()) return false;
|
|
888
|
+
try {
|
|
889
|
+
const target = realpathSync(join(path, entry.name));
|
|
890
|
+
return statSync(target).isDirectory() && allowed.some(root => isWithinRoot(target, root));
|
|
891
|
+
} catch { return false; }
|
|
892
|
+
})
|
|
893
|
+
.map(entry => ({ name: entry.name, path: join(path, entry.name) }))
|
|
894
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
895
|
+
const atRoot = allowed.includes(path);
|
|
896
|
+
return jsonResponse({ path, parent: atRoot ? null : dirname(path), folders });
|
|
897
|
+
} catch {
|
|
898
|
+
return jsonResponse({ error: 'folder_unavailable' }, 404);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
849
902
|
export async function handleProjectsPost(req: Request): Promise<Response> {
|
|
850
903
|
let body: unknown;
|
|
851
904
|
try { body = await req.json(); }
|
|
@@ -996,7 +1049,9 @@ export async function routeRequest(
|
|
|
996
1049
|
const offered = req.headers.get('authorization');
|
|
997
1050
|
const fallbackAuthorized = fallbackToken !== undefined && offered?.startsWith('Bearer ') === true
|
|
998
1051
|
&& compareTokenConstTime(offered.slice('Bearer '.length).trim(), fallbackToken);
|
|
999
|
-
|
|
1052
|
+
const graphPeerAuthorized = (method === 'GET' || method === 'PUT')
|
|
1053
|
+
&& graphAccessForRequest(pathname, method, offered?.startsWith('Bearer ') ? offered.slice(7).trim() : null);
|
|
1054
|
+
if (!(opts.metaApi ? checkAuth(req, opts.metaApi) : fallbackAuthorized) && !graphPeerAuthorized) {
|
|
1000
1055
|
const name = method === 'POST' && (pathname === '/v1/tasks' || pathname === '/v1/reports/ingest') && opts.metaApi
|
|
1001
1056
|
? matchIngestAuthorization(req)?.name
|
|
1002
1057
|
: undefined;
|
|
@@ -1407,7 +1462,12 @@ export async function routeRequest(
|
|
|
1407
1462
|
return handleGraphsValidatePost(req, opts.metaApi);
|
|
1408
1463
|
}
|
|
1409
1464
|
|
|
1410
|
-
// GRAPH-WIZARD — 말 → 그래프 YAML(저장 안 함).
|
|
1465
|
+
// GRAPH-WIZARD — 설치된 팩 목록과 말 → 그래프 YAML(저장 안 함). 같은 소유자 인증.
|
|
1466
|
+
if (method === 'GET' && pathname === '/v1/graphs/wizard/packs') {
|
|
1467
|
+
if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
|
|
1468
|
+
if (!checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1469
|
+
return handleGraphWizardPacksGet(opts.graphWizard);
|
|
1470
|
+
}
|
|
1411
1471
|
if (method === 'POST' && pathname === '/v1/graphs/wizard') {
|
|
1412
1472
|
if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
|
|
1413
1473
|
if (!checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
@@ -1419,6 +1479,10 @@ export async function routeRequest(
|
|
|
1419
1479
|
return handleFabricPlans(req, opts.fabricPlans);
|
|
1420
1480
|
}
|
|
1421
1481
|
|
|
1482
|
+
if (pathname === '/v1/projects/folders' && method === 'GET') {
|
|
1483
|
+
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1484
|
+
return handleProjectFoldersGet(req, opts.projectFolderRoots);
|
|
1485
|
+
}
|
|
1422
1486
|
if (pathname === '/v1/projects' && (method === 'GET' || method === 'POST')) {
|
|
1423
1487
|
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1424
1488
|
return method === 'GET' ? handleProjectsGet() : handleProjectsPost(req);
|
|
@@ -1515,6 +1579,10 @@ export async function routeRequest(
|
|
|
1515
1579
|
...opts.decisions,
|
|
1516
1580
|
});
|
|
1517
1581
|
}
|
|
1582
|
+
if (method === 'POST' && pathname === '/v1/plugins/wizard') {
|
|
1583
|
+
if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1584
|
+
return handlePluginWizardSave(req, opts.metaApi, opts.pluginStateRoot);
|
|
1585
|
+
}
|
|
1518
1586
|
if (method === 'POST' && pathname === '/v1/plugins/install') {
|
|
1519
1587
|
if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1520
1588
|
return handlePluginsInstall(req, opts.metaApi, opts.pluginStateRoot);
|
|
@@ -1534,10 +1602,19 @@ export async function routeRequest(
|
|
|
1534
1602
|
if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1535
1603
|
return handlePluginsRemove(req, pluginRemove[1]!, opts.metaApi, opts.pluginStateRoot);
|
|
1536
1604
|
}
|
|
1605
|
+
if (method === 'PUT' && /^\/v1\/graphs\/[^/]+\/access$/.test(pathname)) {
|
|
1606
|
+
if (!opts.metaApi || bearerCredential(req, opts.metaApi) === undefined) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1607
|
+
return handleGraphAccessPut(pathname, req);
|
|
1608
|
+
}
|
|
1537
1609
|
if ((method === 'PUT' || method === 'POST') && (pathname.startsWith('/v1/graphs/') || pathname === '/v1/graphs')) {
|
|
1538
1610
|
if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
|
|
1539
|
-
|
|
1540
|
-
|
|
1611
|
+
const peerMayEdit = method === 'PUT' && graphAccessForRequest(pathname, method,
|
|
1612
|
+
req.headers.get('authorization')?.startsWith('Bearer ') ? req.headers.get('authorization')!.slice(7).trim() : null);
|
|
1613
|
+
const ownerAuthorized = checkAuth(req, opts.metaApi);
|
|
1614
|
+
if (!ownerAuthorized && !peerMayEdit) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1615
|
+
// A peer save is recorded on the graph so every run path refuses it until the owner approves (W9c run guard).
|
|
1616
|
+
const mutated = await handleGraphsMutation(pathname, req, {}, ownerAuthorized ? null
|
|
1617
|
+
: { peerToken: req.headers.get('authorization')!.slice(7).trim() });
|
|
1541
1618
|
if (mutated) return mutated;
|
|
1542
1619
|
}
|
|
1543
1620
|
const cardClose = /^\/v1\/task-cards\/([^/]+)\/close$/.exec(pathname);
|
|
@@ -2829,6 +2906,10 @@ export async function routeRequest(
|
|
|
2829
2906
|
// surface (`/v1/workflows` · `~/.elanous/workflows-runs/`) covers the same
|
|
2830
2907
|
// user need.
|
|
2831
2908
|
|
|
2909
|
+
if (method === 'GET' && pathname === '/v1/plugins/wizard') {
|
|
2910
|
+
if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
2911
|
+
return handlePluginWizardList(req, opts.metaApi, opts.pluginStateRoot);
|
|
2912
|
+
}
|
|
2832
2913
|
const pluginCredentialsGet = /^\/v1\/plugins\/([^/]+)\/credentials$/.exec(pathname);
|
|
2833
2914
|
if (method === 'GET' && pluginCredentialsGet) {
|
|
2834
2915
|
if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
@@ -2844,6 +2925,10 @@ export async function routeRequest(
|
|
|
2844
2925
|
catch (error) { debug.log('graph.kinds', 'installed-sync-failed', { reason: String(error) }); }
|
|
2845
2926
|
return handleGraphKindsGet(req, opts.metaApi);
|
|
2846
2927
|
}
|
|
2928
|
+
if (method === 'GET' && /^\/v1\/graphs\/[^/]+\/access$/.test(pathname)) {
|
|
2929
|
+
if (!opts.metaApi || bearerCredential(req, opts.metaApi) === undefined) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
2930
|
+
return handleGraphAccess(pathname, req);
|
|
2931
|
+
}
|
|
2847
2932
|
// F-M1 read + P-F1 raw YAML. Writes live in the mutation block and never touch core graphs/.
|
|
2848
2933
|
if (method === 'GET' && (pathname === '/v1/graphs' || /^\/v1\/graphs\/[^/]+$/.test(pathname) || /^\/v1\/graphs\/[^/]+\/yaml$/.test(pathname))) {
|
|
2849
2934
|
return handleGraphsGet(pathname);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { listWizardDrafts, saveWizardDraft, type WizardResearchDraft } from '../../plugins/maker/wizard-generate.js';
|
|
2
3
|
import { dirname, join } from 'node:path';
|
|
3
4
|
import { elanousStateRoot } from '../../autopilot/state-paths.js';
|
|
4
5
|
import { debug } from '../../debug/log.js';
|
|
@@ -120,6 +121,29 @@ export function handlePluginsIndexGet(req: Request, opts: MetaApiOpts, root?: st
|
|
|
120
121
|
return jsonResponse(readMarketIndex(root, configuredTrustedKeys(configPath)));
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
export function handlePluginWizardList(req: Request, opts: MetaApiOpts, root = elanousStateRoot()): Response {
|
|
125
|
+
if (!checkAuth(req, opts)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
126
|
+
return jsonResponse({ plugins: listWizardDrafts(join(root, 'plugins-local')) });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function handlePluginWizardSave(req: Request, opts: MetaApiOpts, root = elanousStateRoot()): Promise<Response> {
|
|
130
|
+
if (!checkAuth(req, opts)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
131
|
+
let body: unknown;
|
|
132
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: 'bad_request' }, 400); }
|
|
133
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) return jsonResponse({ error: 'bad_request' }, 400);
|
|
134
|
+
const input = body as { name?: unknown; draft?: unknown; regenerate?: unknown };
|
|
135
|
+
if (typeof input.name !== 'string' || !/^[a-z0-9][a-z0-9-]{1,31}$/.test(input.name) ||
|
|
136
|
+
!input.draft || typeof input.draft !== 'object' || Array.isArray(input.draft) ||
|
|
137
|
+
(input.regenerate !== undefined && typeof input.regenerate !== 'boolean')) return jsonResponse({ error: 'bad_request' }, 400);
|
|
138
|
+
try {
|
|
139
|
+
saveWizardDraft(join(root, 'plugins-local'), input.name, input.draft as WizardResearchDraft, input.regenerate === true);
|
|
140
|
+
return jsonResponse({ name: input.name, saved: true });
|
|
141
|
+
} catch (error) {
|
|
142
|
+
const reason = error instanceof Error ? error.message : 'invalid draft';
|
|
143
|
+
return jsonResponse({ error: 'invalid_wizard_draft', reason: /already exists|research draft/.test(reason) ? reason : 'invalid draft' }, 422);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
123
147
|
export function handlePluginsGet(req: Request, opts: MetaApiOpts, root?: string): Response {
|
|
124
148
|
if (!checkAuth(req, opts)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
125
149
|
return jsonResponse(listInstalledPlugins(root));
|
package/src/nexus/index.ts
CHANGED
|
@@ -2743,11 +2743,12 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
|
|
|
2743
2743
|
if ((getUserConfig().raw?.decisions as { cards?: unknown } | undefined)?.cards !== false && !isAutonomousRunContext()) {
|
|
2744
2744
|
try {
|
|
2745
2745
|
const { DecisionCardService } = await import('../decisions/decision-cards.js');
|
|
2746
|
-
const { webPushDecisionTransport } = await import('../decisions/web-push-decision-cards.js');
|
|
2746
|
+
const { webPushDecisionTransport, webPushUrgency } = await import('../decisions/web-push-decision-cards.js');
|
|
2747
2747
|
const { listSubscriptions } = await import('../web-push/subscriptions.js');
|
|
2748
2748
|
const { DecisionLedger } = await import('../decisions/decision-ledger.js');
|
|
2749
2749
|
const ledger = new DecisionLedger();
|
|
2750
|
-
const service = new DecisionCardService({ transport: webPushDecisionTransport(ledger), ownerIds: [], ledger
|
|
2750
|
+
const service = new DecisionCardService({ transport: webPushDecisionTransport(ledger), ownerIds: [], ledger,
|
|
2751
|
+
sendFilter: (entry, now) => webPushUrgency(entry, now) !== 'not-urgent', deferredReason: webPushUrgency });
|
|
2751
2752
|
await service.tick();
|
|
2752
2753
|
webPushDecisionTimer = setInterval(() => {
|
|
2753
2754
|
void service.tick().catch((error: unknown) => {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type ClaudePtyMaturity =
|
|
2
|
+
| { readonly mature: true }
|
|
3
|
+
| { readonly mature: false; readonly why: 'too-few-samples' | 'completion-below-70' | 'unknown-screen-above-10' | 'intervention-above-0.2' | 'sample-unreadable' };
|
|
4
|
+
|
|
5
|
+
export type ClaudePtyMaturitySample = {
|
|
6
|
+
readonly runCount: number;
|
|
7
|
+
readonly completedCount: number;
|
|
8
|
+
readonly unknownScreenCount: number;
|
|
9
|
+
readonly interventionCount: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** 최근 런의 계수만 판정한다. 표본을 못 읽거나 계수가 불가능하면 성숙으로 추정하지 않는다. */
|
|
13
|
+
export function judgeClaudePtyMaturity(sample: ClaudePtyMaturitySample | null | undefined): ClaudePtyMaturity {
|
|
14
|
+
if (!sample || ![sample.runCount, sample.completedCount, sample.unknownScreenCount, sample.interventionCount]
|
|
15
|
+
.every((count) => Number.isSafeInteger(count) && count >= 0)
|
|
16
|
+
|| sample.completedCount > sample.runCount || sample.unknownScreenCount > sample.runCount) {
|
|
17
|
+
return { mature: false, why: 'sample-unreadable' };
|
|
18
|
+
}
|
|
19
|
+
if (sample.runCount < 10) return { mature: false, why: 'too-few-samples' };
|
|
20
|
+
if (sample.completedCount / sample.runCount < 0.7) return { mature: false, why: 'completion-below-70' };
|
|
21
|
+
if (sample.unknownScreenCount / sample.runCount > 0.1) return { mature: false, why: 'unknown-screen-above-10' };
|
|
22
|
+
if (sample.interventionCount / sample.runCount > 0.2) return { mature: false, why: 'intervention-above-0.2' };
|
|
23
|
+
return { mature: true };
|
|
24
|
+
}
|
|
@@ -19,9 +19,10 @@
|
|
|
19
19
|
// 지갑을 여는» 일이 없다. (2026-08-13 실측 · RESEARCH-grok-oauth-… §7b)
|
|
20
20
|
|
|
21
21
|
import type { RotationCandidate } from './codex-account-rotation.js';
|
|
22
|
+
import type { ClaudePtyMaturity } from './claude-pty-maturity.js';
|
|
22
23
|
|
|
23
24
|
/** 체인에 놓을 수 있는 칸. ⛔ 모르는 이름은 «조용히 건너뛰지 않고» 거부한다(오타 방어). */
|
|
24
|
-
export const FALLBACK_STEPS = ['codex-rotate', 'grok'] as const;
|
|
25
|
+
export const FALLBACK_STEPS = ['codex-rotate', 'grok', 'claude-pty'] as const;
|
|
25
26
|
export type FallbackStep = (typeof FALLBACK_STEPS)[number];
|
|
26
27
|
|
|
27
28
|
/** 기본 체인 — ⛔⭐ **2026-08-20 에 `grok` 이 «기본»으로 들어왔다**(대표 지시).
|
|
@@ -75,8 +76,8 @@ export type RotationOutcome =
|
|
|
75
76
|
export type FallbackDecision =
|
|
76
77
|
/** codex 계정을 갈아탄다(종전 동작). */
|
|
77
78
|
| { readonly action: 'codex-rotate'; readonly to: RotationCandidate }
|
|
78
|
-
/** 백엔드를 통째로 바꾼다.
|
|
79
|
-
| { readonly action: 'switch-backend'; readonly backend: 'grok' }
|
|
79
|
+
/** 백엔드를 통째로 바꾼다. */
|
|
80
|
+
| { readonly action: 'switch-backend'; readonly backend: 'grok' | 'claude' }
|
|
80
81
|
/** 아무것도 안 한다 — 이유를 «값으로» 남긴다. */
|
|
81
82
|
| { readonly action: 'stay'; readonly why: StayReason };
|
|
82
83
|
|
|
@@ -96,7 +97,9 @@ export type StayReason =
|
|
|
96
97
|
/** 다음 칸이 grok 인데 grok 자격이 «없다». */
|
|
97
98
|
| 'grok-unavailable'
|
|
98
99
|
/** grok 자격은 있는데 «잔량이 소진»됐다 — 자격 부재와 «다른 값»이다. */
|
|
99
|
-
| 'grok-exhausted'
|
|
100
|
+
| 'grok-exhausted'
|
|
101
|
+
/** claude PTY 의 최근 표본이 성숙 관문을 통과하지 못했거나 읽히지 않는다. */
|
|
102
|
+
| 'claude-pty-immature';
|
|
100
103
|
|
|
101
104
|
type FallbackRunPosition =
|
|
102
105
|
| { readonly currentCredentialRateLimited?: false; readonly currentStep?: FallbackStep }
|
|
@@ -120,6 +123,8 @@ export type FallbackInput = {
|
|
|
120
123
|
* (⇒ 그리고 그 사실은 관측에 값으로 남는다 — 「모르고 갔다」를 셀 수 있다.)
|
|
121
124
|
*/
|
|
122
125
|
readonly grokQuota?: 'usable' | 'exhausted' | 'unknown';
|
|
126
|
+
/** 최근 claude PTY 표본의 순수 성숙 판정. 생략·못 읽음은 전환 불가. */
|
|
127
|
+
readonly claudePtyMaturity?: ClaudePtyMaturity;
|
|
123
128
|
/**
|
|
124
129
|
* `reset-credit-available` 에서 체인을 타지 않고 머문다.
|
|
125
130
|
* 기본(생략·false)은 `no-candidate` 와 같이 `codex-rotate` 다음 칸을 찾는다.
|
|
@@ -148,12 +153,12 @@ export type FallbackInput = {
|
|
|
148
153
|
export function decideFallback(input: FallbackInput): FallbackDecision {
|
|
149
154
|
const {
|
|
150
155
|
rotation, chain, currentStep, currentCredentialRateLimited = false,
|
|
151
|
-
grokAvailable, grokQuota = 'unknown', stayOnResetCreditAvailable = false,
|
|
156
|
+
grokAvailable, grokQuota = 'unknown', claudePtyMaturity, stayOnResetCreditAvailable = false,
|
|
152
157
|
} = input;
|
|
153
158
|
|
|
154
159
|
if (currentCredentialRateLimited) {
|
|
155
160
|
if (currentStep === undefined) return { action: 'stay', why: 'chain-exhausted' };
|
|
156
|
-
return stepAfter(chain, currentStep, grokAvailable, grokQuota, 'chain-exhausted');
|
|
161
|
+
return stepAfter(chain, currentStep, grokAvailable, grokQuota, claudePtyMaturity, 'chain-exhausted');
|
|
157
162
|
}
|
|
158
163
|
if (rotation.reason === 'explicit') return { action: 'stay', why: 'explicit' };
|
|
159
164
|
if (rotation.reason === 'not-reached' || (rotation.reason === 'credit-pace' && !('to' in rotation))) {
|
|
@@ -167,13 +172,13 @@ export function decideFallback(input: FallbackInput): FallbackDecision {
|
|
|
167
172
|
// 회전이 답을 냈는데 체인이 codex-rotate 를 «빼» 놨다면 그 뜻을 존중하고
|
|
168
173
|
// 다음 칸으로 간다(회전을 원치 않는 구성).
|
|
169
174
|
if (chain.includes('codex-rotate')) return { action: 'codex-rotate', to: rotation.to };
|
|
170
|
-
return stepAfterCodex(chain, grokAvailable, grokQuota, 'chain-exhausted');
|
|
175
|
+
return stepAfterCodex(chain, grokAvailable, grokQuota, claudePtyMaturity, 'chain-exhausted');
|
|
171
176
|
}
|
|
172
177
|
|
|
173
178
|
// no-candidate | disabled | reset-credit-available(기본) — codex 축이 끝났다. 다음 칸을 본다.
|
|
174
179
|
// ⛔ 다음 칸이 «없을 때»의 이름만 고른다. 진행(stepAfterCodex)은 그대로다.
|
|
175
180
|
const fallbackWhy = stayReasonWhenCodexAxisEnds(rotation.reason === 'credit-pace' ? 'no-candidate' : rotation.reason);
|
|
176
|
-
return stepAfterCodex(chain, grokAvailable, grokQuota, fallbackWhy);
|
|
181
|
+
return stepAfterCodex(chain, grokAvailable, grokQuota, claudePtyMaturity, fallbackWhy);
|
|
177
182
|
}
|
|
178
183
|
|
|
179
184
|
/** codex 축이 끝났는데 체인 다음 칸도 없을 때 머무는 이유.
|
|
@@ -191,9 +196,10 @@ function stepAfterCodex(
|
|
|
191
196
|
chain: readonly FallbackStep[],
|
|
192
197
|
grokAvailable: boolean,
|
|
193
198
|
grokQuota: 'usable' | 'exhausted' | 'unknown',
|
|
199
|
+
claudePtyMaturity: ClaudePtyMaturity | undefined,
|
|
194
200
|
whyIfNone: StayReason,
|
|
195
201
|
): FallbackDecision {
|
|
196
|
-
return stepAfter(chain, 'codex-rotate', grokAvailable, grokQuota, whyIfNone);
|
|
202
|
+
return stepAfter(chain, 'codex-rotate', grokAvailable, grokQuota, claudePtyMaturity, whyIfNone);
|
|
197
203
|
}
|
|
198
204
|
|
|
199
205
|
function stepAfter(
|
|
@@ -201,20 +207,27 @@ function stepAfter(
|
|
|
201
207
|
currentStep: FallbackStep | undefined,
|
|
202
208
|
grokAvailable: boolean,
|
|
203
209
|
grokQuota: 'usable' | 'exhausted' | 'unknown',
|
|
210
|
+
claudePtyMaturity: ClaudePtyMaturity | undefined,
|
|
204
211
|
whyIfNone: StayReason,
|
|
205
212
|
): FallbackDecision {
|
|
206
213
|
const idx = currentStep === undefined ? -1 : chain.indexOf(currentStep);
|
|
207
214
|
const rest = idx === -1 ? chain : chain.slice(idx + 1);
|
|
215
|
+
let unavailableWhy: StayReason | undefined;
|
|
208
216
|
for (const step of rest) {
|
|
209
217
|
if (step === 'grok') {
|
|
210
|
-
//
|
|
211
|
-
if (!grokAvailable)
|
|
212
|
-
//
|
|
213
|
-
if (grokQuota === 'exhausted')
|
|
218
|
+
// 자격 부재와 소진은 다른 값이다. 다음 칸이 있으면 그 칸을 살핀다.
|
|
219
|
+
if (!grokAvailable) { unavailableWhy = 'grok-unavailable'; continue; }
|
|
220
|
+
// 「모른다」는 통과시킨다 — 못 읽었다고 갈 곳을 없애지 않는다.
|
|
221
|
+
if (grokQuota === 'exhausted') { unavailableWhy = 'grok-exhausted'; continue; }
|
|
214
222
|
return { action: 'switch-backend', backend: 'grok' };
|
|
215
223
|
}
|
|
224
|
+
if (step === 'claude-pty') {
|
|
225
|
+
// 미성숙·못 읽음은 «건너뛴다» — 뒤에 다른 칸이 있으면 그 칸을 본다(없으면 이 사유로 머문다).
|
|
226
|
+
if (claudePtyMaturity?.mature !== true) { unavailableWhy = 'claude-pty-immature'; continue; }
|
|
227
|
+
return { action: 'switch-backend', backend: 'claude' };
|
|
228
|
+
}
|
|
216
229
|
}
|
|
217
|
-
return { action: 'stay', why: whyIfNone };
|
|
230
|
+
return { action: 'stay', why: unavailableWhy ?? whyIfNone };
|
|
218
231
|
}
|
|
219
232
|
|
|
220
233
|
/** 사람이 읽는 한 줄 — 관측·CLI 표면용. ⛔ 토큰·계정 홈은 넣지 않는다. */
|
|
@@ -4,6 +4,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
4
4
|
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { dirname, join, resolve, sep } from 'node:path';
|
|
6
6
|
import { userConfigPath } from '../../user-config.js';
|
|
7
|
+
import { scanKnowledgePackDlp } from '../../mss/logging/redaction.js';
|
|
7
8
|
import { loadPluginManifestFromDir } from '../core/manifest.js';
|
|
8
9
|
import { LEAK_MARKERS, loadPrivateRedactions, privateIdentifierMarkers, scanLeaks } from '../../../scripts/public-export.js';
|
|
9
10
|
|
|
@@ -56,6 +57,14 @@ export function bundleInstalledWizardPlugin(installedPath: string, outputDir: st
|
|
|
56
57
|
if (!redactions) throw new Error('private export redaction list unavailable; market bundle refused');
|
|
57
58
|
const hits = scanLeaks(source, files, [...LEAK_MARKERS, ...privateIdentifierMarkers(redactions)]);
|
|
58
59
|
if (hits.length) throw new Error(`market bundle leak: ${hits.map(hit => `${hit.file}:${hit.line} ${hit.marker}`).join(', ')}`);
|
|
60
|
+
const dlpHits: string[] = [];
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
const text = readFileSync(join(source, file), 'utf8');
|
|
63
|
+
const reasons = scanKnowledgePackDlp(text);
|
|
64
|
+
if (text.includes('\u0000')) reasons.push('unscannable-content');
|
|
65
|
+
if (reasons.length) dlpHits.push(`${file}: ${reasons.join(', ')}`);
|
|
66
|
+
}
|
|
67
|
+
if (dlpHits.length) throw new Error(`market bundle DLP: ${dlpHits.join('; ')}`);
|
|
59
68
|
const config: unknown = existsSync(configPath) ? JSON.parse(readFileSync(configPath, 'utf8')) : {};
|
|
60
69
|
if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error('invalid market signing config');
|
|
61
70
|
const signing = (config as { market?: { signing?: { keyId?: unknown; privateKey?: unknown } } }).market?.signing;
|
|
@@ -146,7 +146,10 @@ export async function validatePluginDir(dir: string): Promise<string[]> {
|
|
|
146
146
|
catch (error) { errors.push(`recipes.yaml 파싱 실패: ${String(error)}`); }
|
|
147
147
|
if (!isRecord(recipes)) errors.push('recipes.yaml 은 맵이어야 한다');
|
|
148
148
|
else for (const [id, recipe] of Object.entries(recipes)) {
|
|
149
|
-
if (!isRecord(recipe) ||
|
|
149
|
+
if (!isRecord(recipe) ||
|
|
150
|
+
(!(typeof recipe.command === 'string' && recipe.command.trim()) && !(typeof recipe.approval === 'string' && recipe.approval.trim())) ||
|
|
151
|
+
(recipe.command !== undefined && (typeof recipe.command !== 'string' || !recipe.command.trim())) ||
|
|
152
|
+
(recipe.approval !== undefined && (typeof recipe.approval !== 'string' || !recipe.approval.trim())) ||
|
|
150
153
|
(recipe.timeout_ms !== undefined && (!Number.isSafeInteger(recipe.timeout_ms) || Number(recipe.timeout_ms) <= 0))) errors.push(`잘못된 recipe: ${id}`);
|
|
151
154
|
}
|
|
152
155
|
for (const graph of Array.isArray(graphs) ? graphs : []) {
|
|
@@ -162,11 +165,14 @@ export async function validatePluginDir(dir: string): Promise<string[]> {
|
|
|
162
165
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(template.graphId)) errors.push(`잘못된 graph_id: ${template.graphId}`);
|
|
163
166
|
if (template.terminalNodes.length !== 2 || !template.terminalNodes.includes('done') || !template.terminalNodes.includes('failed')) errors.push('terminal_nodes 는 done, failed 이어야 한다');
|
|
164
167
|
const executionNodes = template.nodes.filter(node => !template.terminalNodes.includes(node.nodeId));
|
|
165
|
-
if (executionNodes.length < 2
|
|
168
|
+
if (executionNodes.length < 2) errors.push(`실행 노드는 2개 이상이어야 한다: ${executionNodes.length}개`);
|
|
166
169
|
for (const node of template.nodes) {
|
|
167
170
|
if (node.recipe.startsWith('cmd:')) {
|
|
168
171
|
const id = node.recipe.slice(4);
|
|
169
|
-
if (!isRecord(recipes) || !isRecord(recipes[id]) || typeof recipes[id].command !== 'string' || !recipes[id].command) errors.push(`recipe ${id} 없음`);
|
|
172
|
+
if (!isRecord(recipes) || !isRecord(recipes[id]) || recipes[id].approval !== undefined || typeof recipes[id].command !== 'string' || !recipes[id].command.trim()) errors.push(`recipe ${id} 없음`);
|
|
173
|
+
} else if (node.recipe.startsWith('approval:')) {
|
|
174
|
+
const id = node.recipe.slice(9);
|
|
175
|
+
if (!isRecord(recipes) || !isRecord(recipes[id]) || typeof recipes[id].approval !== 'string' || !recipes[id].approval.trim()) errors.push(`approval recipe ${id} 없음`);
|
|
170
176
|
} else if (node.recipe !== 'none') errors.push(`지원하지 않는 recipe: ${node.recipe}`);
|
|
171
177
|
}
|
|
172
178
|
} catch (error) { errors.push(`${graph} 파싱 실패: ${String(error)}`); }
|