elanous 0.2.7 → 0.2.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/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/bFI5EEwg7T6aG88ibX4Zq/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8630-cdfc61e82c653ffc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8894-e263b332208e3957.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9454-139040d37c79dbc9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-fb7c2d458f32fcad.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-35e2713597dd24d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-918b656f064577ba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-624f7a2f89b60c8f.js → page-f972fc1c00a62e88.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-5a5fb385a4df0ffe.js → page-5f30e00693527ebd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-80497e93a878ddc5.js → page-7fbae173b3c103a7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-7457f52683562161.js → page-3b4edcce364062ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-c5794f484c251f44.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-1130d8eefcd87e76.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-973996d584f008c1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-b7dcf5d48a854b76.js → page-016e2bda04eecb1f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-51480757e7d68b92.js → page-b4d6d9a3b56f2940.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-ec49a466121233ba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-f72db356744ceade.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-a6fd9b50d1b85378.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-8b0402a46a749a3d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-07267f1391b96aba.js → page-037f83e2a1edc8e2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-55d50e9b0006a91d.js → page-adfbd2ba9598e59a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-46fedff57d4bec67.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-0cf318e9dcee2bbf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-899b327cb8b6845c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-6c1e6ba743a29dfe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/{page-a7e37af7d0399cb8.js → page-1a8e4807292f6b17.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-28deba6bcb834916.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-de68e2205794037d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-15e5a15db84c56aa.js → page-c71c33a573230672.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-a2e572e4511f2a16.js → page-0df30e0c7e50148a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-4fc56790fe0d2682.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-59df2aebb38a7c1e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4647be5dbcbd0795.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-e025892bda74b743.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a0955de73b89f078.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-c1c95739c0bff3b8.js → page-369846d2921af584.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-72cbc1c8cdd0f65d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-71d18eec33b9c667.js → page-77b28bfb0996d5ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-3220c4934f49c6c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-2c4e59079ee357ac.js → page-90670589b87abea8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-7df9243ed021a4cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-5d422f38c052967e.js → page-4498c3853f4f688d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-35bedde6d885f08f.js +1 -0
- package/apps/pwa/out/_next/static/css/f92f63a5df0ba229.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +30 -29
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +30 -29
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +30 -29
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +27 -26
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +27 -26
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +27 -26
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +30 -29
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +30 -29
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +30 -29
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +30 -29
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +30 -29
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +30 -29
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +27 -26
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +30 -29
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +30 -29
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +27 -26
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +27 -26
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +30 -29
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +30 -29
- package/apps/pwa/out/ops/checklist/index.html +1 -0
- package/apps/pwa/out/ops/checklist/index.txt +30 -0
- package/apps/pwa/out/ops/release/index.html +1 -0
- package/apps/pwa/out/ops/release/index.txt +30 -0
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +28 -27
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +30 -29
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +27 -26
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +30 -29
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +30 -29
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +30 -29
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +30 -29
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +30 -29
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +27 -26
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +30 -29
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +30 -29
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +30 -29
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +30 -29
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +30 -29
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +30 -29
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +30 -29
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +30 -29
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +3 -1
- package/graphs/test-diet/nightly-audit.yaml +18 -0
- package/graphs/test-diet/recipes.yaml +12 -0
- package/graphs/test-diet/test-diet.yaml +22 -0
- package/package.json +2 -1
- package/persona-presets/_index.yaml +10 -0
- package/persona-presets/_schema.json +146 -0
- package/persona-presets/dana.yaml +26 -0
- package/persona-presets/haru.yaml +27 -0
- package/persona-presets/kai.yaml +27 -0
- package/persona-presets/leo.yaml +27 -0
- package/persona-presets/mira.yaml +30 -0
- package/persona-presets/nari.yaml +27 -0
- package/persona-presets/sora.yaml +27 -0
- package/persona-presets/yuna.yaml +27 -0
- package/plugins/doc-draft/graphs/run-step.ts +1 -1
- package/plugins/geo-check/graphs/run-step.ts +34 -10
- package/scripts/field-feed/deliver-node.ts +8 -3
- package/scripts/field-feed/lib.ts +2 -0
- package/scripts/install.sh +20 -11
- package/scripts/persona-presets/lint.ts +79 -0
- package/scripts/release-loop/checklist-gate-node.ts +2 -2
- package/scripts/release-loop/cutoff.ts +20 -2
- package/scripts/release-loop/export-check-node.ts +85 -0
- package/scripts/release-loop/gate-node.ts +45 -7
- package/scripts/release-loop/node-verdict.ts +8 -1
- package/scripts/release-loop/npm-publish-node.ts +10 -4
- package/scripts/release-loop/ops-upgrade-node.ts +11 -3
- package/scripts/release-loop/publish-node.ts +23 -1
- package/scripts/release-loop/shard-plan.ts +17 -0
- package/scripts/storyboard/storyboard.ts +167 -0
- package/scripts/test-diet/lib.ts +89 -0
- package/scripts/test-diet/node.ts +144 -0
- package/src/acp/auth-methods.ts +30 -0
- package/src/acp/server.ts +5 -5
- package/src/auth/setup-link-tokens.fixture.ts +19 -0
- package/src/auth/setup-link-tokens.ts +153 -0
- package/src/budget/unified-usage.ts +75 -0
- package/src/channel-bot-token.ts +37 -10
- package/src/channel-import/detect.ts +191 -0
- package/src/channel-import/run.ts +111 -0
- package/src/chat/index.ts +9 -9
- package/src/cli/config-test-sync.ts +4 -0
- package/src/cli/decisions-cli.ts +13 -3
- package/src/cli/doctor-bundle.ts +128 -0
- package/src/cli/doctor-cli.ts +36 -1
- package/src/cli/doctor-local-llm.ts +95 -0
- package/src/cli/import-cli.ts +60 -0
- package/src/cli/mcp-login.ts +13 -4
- package/src/cli/op-proxy-sync.ts +83 -0
- package/src/cli/persona-cli.ts +116 -0
- package/src/cli/pr-cli.ts +6 -2
- package/src/cli/release-cli.ts +49 -8
- package/src/cli/skills-cli.ts +173 -0
- package/src/cli/start-cli.ts +13 -4
- package/src/codex/setup.ts +15 -13
- package/src/dashboard/dashboard-status-lines.ts +45 -0
- package/src/dashboard/first-screen-band.ts +35 -0
- package/src/dashboard/index.ts +87 -14
- package/src/dashboard/provider-slash-runtime.ts +28 -11
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +12 -2
- package/src/dashboard/slash-runtime/help-from-registry.ts +4 -2
- package/src/dashboard/turn-stream-formatter.ts +1 -1
- package/src/decisions/decision-cards.ts +291 -0
- package/src/decisions/decision-ledger.ts +5 -2
- package/src/decisions/discord-decision-cards.ts +133 -0
- package/src/decisions/telegram-decision-cards.ts +100 -0
- package/src/directives/version-at.ts +77 -0
- package/src/discord.ts +17 -0
- package/src/domains/outbound-alert.ts +44 -1
- package/src/domains/repeated-failure.ts +1 -1
- package/src/error-report/report.ts +200 -0
- package/src/exec-requests/planner.ts +8 -2
- package/src/exec-requests/runner.ts +64 -24
- package/src/exec-requests/store.ts +2 -0
- package/src/field/field-feed-auto.ts +48 -0
- package/src/field/field-reel.ts +8 -1
- package/src/hooks/error-report.ts +210 -0
- package/src/hooks/receiver.ts +41 -1
- package/src/import/apply.ts +128 -0
- package/src/import/plan.ts +197 -0
- package/src/index.ts +98 -11
- package/src/input-core/text-entry.ts +13 -5
- package/src/intake-plane/seat-answer.ts +96 -0
- package/src/intake-plane/seat-doc-route.ts +17 -3
- package/src/maturity/feature-maturity.ts +2 -0
- package/src/nexus/api/consult-requests.ts +19 -8
- package/src/nexus/api/error-reports.ts +108 -0
- package/src/nexus/api/http-server.ts +41 -1
- package/src/nexus/api/meta-api.ts +27 -0
- package/src/nexus/api/operator.ts +61 -0
- package/src/nexus/api/ops-api.ts +139 -0
- package/src/nexus/api/ops-seats-sources.ts +65 -0
- package/src/nexus/api/ops-seats.ts +114 -0
- package/src/nexus/api/outbound-report.ts +7 -0
- package/src/nexus/api/public-routes.ts +14 -1
- package/src/nexus/api/running-runs-background.ts +68 -0
- package/src/nexus/api/running-runs-child.ts +6 -0
- package/src/nexus/api/setup-claim.ts +21 -0
- package/src/nexus/api/share-captures.ts +91 -0
- package/src/nexus/api/skill-problems.ts +56 -0
- package/src/nexus/api/terminals.ts +24 -8
- package/src/nexus/config/secrets/file-backend.ts +56 -18
- package/src/nexus/index.ts +25 -1
- package/src/nexus/setup-status.ts +7 -2
- package/src/oauth/browser-availability.ts +14 -0
- package/src/oauth/codex-quota-policy.ts +16 -0
- package/src/onboarding/continue-here.ts +13 -8
- package/src/onboarding/first-run-doctor.ts +54 -0
- package/src/onboarding/web-first.ts +8 -1
- package/src/onboarding.ts +66 -12
- package/src/persona/presets.ts +105 -0
- package/src/release-loop/checklist.ts +33 -69
- package/src/release-loop/feature-store.ts +327 -0
- package/src/release-loop/manifest.ts +59 -5
- package/src/seat-dispatch/ceo-commands.ts +65 -0
- package/src/self-dev/orchestrate.ts +18 -11
- package/src/shell-env-bootstrap.ts +44 -7
- package/src/skills/connect-claude-plugin.ts +142 -0
- package/src/skills/connect.ts +70 -0
- package/src/skills/index.ts +29 -1
- package/src/skills/repair.ts +124 -0
- package/src/skills/runner.ts +11 -2
- package/src/skills/tools/usage-slash.ts +8 -7
- package/src/skills/trigger-extract.ts +1 -1
- package/src/task-orchestrator/surfaces/pod-command-job.ts +16 -8
- package/src/telegram-commands.ts +29 -0
- package/src/telegram-run.ts +83 -13
- package/src/telegram.ts +34 -0
- package/src/thinking-line.ts +28 -20
- package/src/tui/chat/execution-badge.ts +1 -1
- package/src/user-config.ts +111 -13
- package/src/ux-sim/turn-states.ts +1 -1
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/4836-2eba6079f31b88d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8519-f6b5d8a5833a7603.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8894-e92491a164192cc1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9740-67051141115ce775.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d5ebde04844689e6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-10ed24f413ed66e9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-211810b6c93ab66a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-cee86394de407aa6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-4ff8edab44196b8c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-92e4f44428d5e290.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-2a05dfe80c8bdf16.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-d9979633aa82f8e6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-30533117beb53625.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-f7857826c2f97ad7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-9699b1786be6171d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-43dd772aee8af75f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-973ab7af59c718b7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a9f8d3865b91e436.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-dd107bf1c7d52a90.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d42b99cdfb9f27c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-2764ff40dad5a995.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-c5f1152db116ff96.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-f933bca81ea38a6f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-a7aaffcf771662d8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-56adbe1a1f1379bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a81f113f3ef89d72.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-fc27b00e20733146.js +0 -1
- package/apps/pwa/out/_next/static/css/f537bdb19c69306a.css +0 -1
- package/apps/pwa/out/_next/static/keajW7DDyghEs1y8oJe6T/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{keajW7DDyghEs1y8oJe6T → bFI5EEwg7T6aG88ibX4Zq}/_ssgManifest.js +0 -0
|
@@ -10,6 +10,7 @@ import { getElanousConfigDir } from '../elanous-config-dir.js';
|
|
|
10
10
|
import type { ExecRequest } from '../exec-requests/store.js';
|
|
11
11
|
import { resolveDaemonEndpoint } from '../nexus/daemon-endpoint.js';
|
|
12
12
|
import { parseSeatAddress, resolveSeat } from '../seat-address/seat-address.js';
|
|
13
|
+
import { answerAsSeat, isNoGraphFailure } from './seat-answer.js';
|
|
13
14
|
|
|
14
15
|
const POLL_MS = 5_000;
|
|
15
16
|
const TIMEOUT_MS = 30 * 60_000;
|
|
@@ -34,7 +35,9 @@ export interface SeatDocDeps {
|
|
|
34
35
|
sleep?: (ms: number) => Promise<void>;
|
|
35
36
|
now?: () => number;
|
|
36
37
|
pwaUrl?: () => string | undefined;
|
|
37
|
-
log?: (event: 'submitted' | 'research' | 'settled' | 'delivered' | 'delivery-failed', data: Record<string, unknown>) => void;
|
|
38
|
+
log?: (event: 'submitted' | 'research' | 'settled' | 'delivered' | 'delivery-failed' | 'seat-answer', data: Record<string, unknown>) => void;
|
|
39
|
+
/** EV12d — the seat's own answer when no installed graph fits (default = role file ⊕ owned checklist ⊕ recent, one LLM call). */
|
|
40
|
+
seatAnswer?: (seatAddress: string, question: string) => Promise<{ title: string; text: string } | null>;
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
export function isSeatDocRequest(text: string): boolean {
|
|
@@ -194,9 +197,20 @@ export async function submitSeatDocRequest({ text, reportTo, deps = {} }: {
|
|
|
194
197
|
}
|
|
195
198
|
log('settled', { id, status: result?.status ?? (lookupAvailable ? 'timeout' : 'lookup-unavailable'), lastConfirmedStatus });
|
|
196
199
|
const links = result?.status === 'done' ? await fileLinks(id, result, deps) : new Map<string, string>();
|
|
200
|
+
let reply = message(id, result, withoutResearch, lookupAvailable, accessiblePwaUrl((deps.pwaUrl ?? chatPwaUrl)()), links);
|
|
201
|
+
if (isNoGraphFailure(result)) {
|
|
202
|
+
// EV12d — a question with no fitting graph gets the seat's own answer, not «문서 요청 실패».
|
|
203
|
+
const seat = result!.seats![0]!.seat;
|
|
204
|
+
try {
|
|
205
|
+
const answer = await (deps.seatAnswer ?? answerAsSeat)(seat, body);
|
|
206
|
+
if (answer) reply = `${answer.title} 답 (접수번호 ${id})\n${answer.text}`;
|
|
207
|
+
log('seat-answer', { id, seat, answered: !!answer });
|
|
208
|
+
} catch (error) {
|
|
209
|
+
log('seat-answer', { id, seat, answered: false, reason: String(error).slice(0, 200) });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
197
212
|
try {
|
|
198
|
-
const sent = await (deps.sendOutbound ?? sendOutbound)(
|
|
199
|
-
accessiblePwaUrl((deps.pwaUrl ?? chatPwaUrl)()), links), 'report', reportTo);
|
|
213
|
+
const sent = await (deps.sendOutbound ?? sendOutbound)(reply, 'report', reportTo);
|
|
200
214
|
log(sent ? 'delivered' : 'delivery-failed', { id });
|
|
201
215
|
} catch (error) {
|
|
202
216
|
log('delivery-failed', { id, reason: String(error).slice(0, 200) });
|
|
@@ -38,6 +38,8 @@ export const FEATURE_MATURITY = {
|
|
|
38
38
|
'/design-check': { pwa: 'tool' },
|
|
39
39
|
'/control': { pwa: 'tool' },
|
|
40
40
|
'/scheduler': { pwa: 'ops' },
|
|
41
|
+
'/ops/release': { pwa: 'ops' },
|
|
42
|
+
'/ops/checklist': { pwa: 'ops' },
|
|
41
43
|
'/dashboard': { pwa: 'ops' },
|
|
42
44
|
'/bots': { pwa: 'ops' },
|
|
43
45
|
'/botlab': { pwa: 'ops' },
|
|
@@ -2,7 +2,8 @@ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, re
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { debug } from '../../debug/log.js';
|
|
5
|
-
import {
|
|
5
|
+
import { routeOutbound } from '../outbound/router.js';
|
|
6
|
+
import { getUserConfig } from '../../user-config.js';
|
|
6
7
|
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
7
8
|
import { jsonResponse } from './json-response.js';
|
|
8
9
|
|
|
@@ -22,7 +23,10 @@ interface ConsultRequest {
|
|
|
22
23
|
export interface ConsultRequestsDeps {
|
|
23
24
|
root?: () => string;
|
|
24
25
|
now?: () => string;
|
|
25
|
-
|
|
26
|
+
/** Owner alert. Default = the daemon's own in-process router (the same one `/v1/outbound` uses).
|
|
27
|
+
* ⛔ Never `sendOutbound` here: it is a synchronous curl to this very daemon's `/v1/outbound`, so inside the
|
|
28
|
+
* daemon it blocks the event loop until it times out (~25 s · `unreachable`) and the alert is lost (CS1 · 10-01). */
|
|
29
|
+
send?: (text: string, kind: string) => unknown;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
function file(root: string): string {
|
|
@@ -107,11 +111,18 @@ export async function handleConsultRequests(req: Request, deps: ConsultRequestsD
|
|
|
107
111
|
return jsonResponse({ error: 'journal-unavailable' }, 503);
|
|
108
112
|
}
|
|
109
113
|
debug.log('consult.request', 'accepted', { receiptId: entry.receiptId });
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
);
|
|
115
|
-
} catch { /* the local receipt remains accepted if delivery is unavailable */ }
|
|
114
|
+
const alert = `📮 상담 문의 ${entry.receiptId} · ${entry.name}(${entry.kind === 'personal' ? '개인' : (entry.org ?? '회사')}) · 관심 ${entry.interest}\n앱에서 보기`;
|
|
115
|
+
// Fire and forget — the receipt is already on disk; the answer must not wait for the channel.
|
|
116
|
+
void Promise.resolve()
|
|
117
|
+
.then(() => (deps.send ?? notifyInProcess)(alert, 'alert'))
|
|
118
|
+
.catch((error: unknown) => { debug.log('consult.request', 'notify-failed', { receiptId: entry.receiptId, reason: error instanceof Error ? error.message : String(error) }); });
|
|
116
119
|
return jsonResponse({ receiptId: entry.receiptId, receivedAt: entry.receivedAt }, 202);
|
|
117
120
|
}
|
|
121
|
+
|
|
122
|
+
async function notifyInProcess(text: string, kind: string): Promise<void> {
|
|
123
|
+
const result = await routeOutbound(getUserConfig(), { text, markdown: false, kind });
|
|
124
|
+
debug.log('consult.request', result.delivered ? 'notified' : 'notify-failed', {
|
|
125
|
+
delivered: result.delivered,
|
|
126
|
+
channels: result.channels.map((channel) => `${channel.type}:${channel.ok ? 'ok' : 'fail'}`),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// ER2 — the Primary side of error reports. The bot VM forwards each accepted report here over the tailnet
|
|
2
|
+
// (ingest token). It is stored under <instance>/error-reports/YYYY/MM/DD/<reportId>.json for 30 days, and the
|
|
3
|
+
// operator gets one Telegram line per distinct error per hour — never the message, stack or identity.
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { debug } from '../../debug/log.js';
|
|
7
|
+
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
8
|
+
import type { ReceivedReport } from '../../hooks/error-report.js';
|
|
9
|
+
import { validateErrorReport } from '../../hooks/error-report.js';
|
|
10
|
+
import { jsonResponse } from './json-response.js';
|
|
11
|
+
import { routeOutboundInProcess } from './outbound-report.js';
|
|
12
|
+
|
|
13
|
+
export const ERROR_REPORT_KEEP_DAYS = 30;
|
|
14
|
+
const DAY_MS = 86_400_000;
|
|
15
|
+
|
|
16
|
+
export interface ErrorReportIngestDeps {
|
|
17
|
+
root?: () => string;
|
|
18
|
+
now?: () => number;
|
|
19
|
+
/** Operator alert. Default = the daemon's in-process router (never a self-call · OB8). */
|
|
20
|
+
alert?: (text: string, kind: string) => Promise<boolean>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function storeDir(root: string): string { return join(root, 'error-reports'); }
|
|
24
|
+
|
|
25
|
+
/** Removes day folders older than the keep window. Returns how many days were removed. */
|
|
26
|
+
export function pruneErrorReports(root: string, now: number): number {
|
|
27
|
+
const base = storeDir(root);
|
|
28
|
+
if (!existsSync(base)) return 0;
|
|
29
|
+
let removed = 0;
|
|
30
|
+
for (const year of readdirSync(base).filter((n) => /^\d{4}$/.test(n))) {
|
|
31
|
+
for (const month of readdirSync(join(base, year)).filter((n) => /^\d{2}$/.test(n))) {
|
|
32
|
+
for (const day of readdirSync(join(base, year, month)).filter((n) => /^\d{2}$/.test(n))) {
|
|
33
|
+
const at = Date.parse(`${year}-${month}-${day}T00:00:00Z`);
|
|
34
|
+
if (Number.isFinite(at) && now - at > ERROR_REPORT_KEEP_DAYS * DAY_MS + DAY_MS) {
|
|
35
|
+
rmSync(join(base, year, month, day), { recursive: true, force: true });
|
|
36
|
+
removed++;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return removed;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One alert per dedupe per hour, remembered on disk so a restart does not repeat it. */
|
|
45
|
+
function shouldAlert(root: string, dedupe: string, now: number): boolean {
|
|
46
|
+
const path = join(storeDir(root), 'alerts.json');
|
|
47
|
+
let seen: Record<string, number> = {};
|
|
48
|
+
try { seen = JSON.parse(readFileSync(path, 'utf8')) as Record<string, number>; } catch { /* first alert */ }
|
|
49
|
+
for (const [k, t] of Object.entries(seen)) if (now - t > DAY_MS) delete seen[k];
|
|
50
|
+
const last = seen[dedupe];
|
|
51
|
+
if (last !== undefined && now - last < 3600_000) return false;
|
|
52
|
+
seen[dedupe] = now;
|
|
53
|
+
mkdirSync(storeDir(root), { recursive: true, mode: 0o700 });
|
|
54
|
+
writeFileSync(path, JSON.stringify(seen), { mode: 0o600 });
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function errorReportAlertText(item: ReceivedReport, count24h: number): string {
|
|
59
|
+
const r = item.report;
|
|
60
|
+
return `🧯 오류 보고 ${r.code} · v${r.app.version} · ${r.app.surface} · 같은 오류 24h ${count24h}회 · ${item.reportId}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Same dedupe stored in the last 24 h, counted from the store itself. */
|
|
64
|
+
function count24h(root: string, dedupe: string, now: number): number {
|
|
65
|
+
let n = 0;
|
|
66
|
+
for (const offset of [0, 1]) {
|
|
67
|
+
const d = new Date(now - offset * DAY_MS);
|
|
68
|
+
const dir = join(storeDir(root), String(d.getUTCFullYear()), String(d.getUTCMonth() + 1).padStart(2, '0'), String(d.getUTCDate()).padStart(2, '0'));
|
|
69
|
+
if (!existsSync(dir)) continue;
|
|
70
|
+
for (const name of readdirSync(dir).filter((f) => f.endsWith('.json'))) {
|
|
71
|
+
try {
|
|
72
|
+
const item = JSON.parse(readFileSync(join(dir, name), 'utf8')) as ReceivedReport;
|
|
73
|
+
if (item.dedupe === dedupe && now - Date.parse(item.receivedAt) < DAY_MS) n++;
|
|
74
|
+
} catch { /* a broken file is not counted */ }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return n;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** POST /v1/reports/ingest — the caller (router) has already checked the ingest token. */
|
|
81
|
+
export async function handleErrorReportIngest(req: Request, deps: ErrorReportIngestDeps = {}): Promise<Response> {
|
|
82
|
+
const root = (deps.root ?? effectiveInstanceRoot)();
|
|
83
|
+
const now = (deps.now ?? Date.now)();
|
|
84
|
+
let body: unknown;
|
|
85
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: 'invalid', field: 'body' }, 400); }
|
|
86
|
+
const item = body as Partial<ReceivedReport>;
|
|
87
|
+
if (typeof item.reportId !== 'string' || !/^er_[0-9a-z]{26}$/.test(item.reportId)) return jsonResponse({ error: 'invalid', field: 'reportId' }, 400);
|
|
88
|
+
if (typeof item.dedupe !== 'string' || !/^[0-9a-f]{12}$/.test(item.dedupe)) return jsonResponse({ error: 'invalid', field: 'dedupe' }, 400);
|
|
89
|
+
if (typeof item.receivedAt !== 'string' || !Number.isFinite(Date.parse(item.receivedAt))) return jsonResponse({ error: 'invalid', field: 'receivedAt' }, 400);
|
|
90
|
+
const checked = validateErrorReport(item.report);
|
|
91
|
+
if (!checked.ok) return jsonResponse({ error: 'invalid', field: `report.${checked.field}` }, 400);
|
|
92
|
+
const stored: ReceivedReport = { reportId: item.reportId, dedupe: item.dedupe, receivedAt: item.receivedAt, report: checked.report };
|
|
93
|
+
const at = new Date(Date.parse(stored.receivedAt));
|
|
94
|
+
const dir = join(storeDir(root), String(at.getUTCFullYear()), String(at.getUTCMonth() + 1).padStart(2, '0'), String(at.getUTCDate()).padStart(2, '0'));
|
|
95
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
96
|
+
writeFileSync(join(dir, `${stored.reportId}.json`), JSON.stringify(stored), { mode: 0o600 });
|
|
97
|
+
let pruned = 0;
|
|
98
|
+
try { pruned = pruneErrorReports(root, now); } catch { /* pruning never blocks a receipt */ }
|
|
99
|
+
const count = count24h(root, stored.dedupe, now);
|
|
100
|
+
const alerted = shouldAlert(root, stored.dedupe, now);
|
|
101
|
+
debug.log('error-report.ingest', 'stored', { code: stored.report.code, surface: stored.report.app.surface, count24h: count, alerted, pruned });
|
|
102
|
+
if (alerted) {
|
|
103
|
+
const alert = deps.alert ?? routeOutboundInProcess;
|
|
104
|
+
void Promise.resolve().then(() => alert(errorReportAlertText(stored, count), 'alert'))
|
|
105
|
+
.catch((error: unknown) => debug.log('error-report.ingest', 'alert-failed', { reason: error instanceof Error ? error.message : String(error) }));
|
|
106
|
+
}
|
|
107
|
+
return jsonResponse({ stored: true, reportId: stored.reportId, count }, 202);
|
|
108
|
+
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// Port policy (N1-2 decision): auto-pick from 31415 upward. Actual port
|
|
16
16
|
// is recorded in runtime.json + printed in boot banner.
|
|
17
17
|
|
|
18
|
+
import { setInProcessOutbound } from '../../domains/outbound-alert.js';
|
|
18
19
|
import { getUserConfig } from '../../user-config.js';
|
|
19
20
|
import type { TelegramEvent } from '../../workflow-runtime/triggers/telegram-source.js';
|
|
20
21
|
import { spawnSync } from 'node:child_process';
|
|
@@ -154,11 +155,14 @@ import {
|
|
|
154
155
|
import { handleBuildsGet, parseBuildsPath } from './builds-api.js';
|
|
155
156
|
import { handleHarnessAskPost, handleHarnessAskStatusGet, handleHarnessRunEventsGet, handleHarnessRunScreenGet, handleHarnessRunsGet, handleHarnessStopPost } from './harness-api.js';
|
|
156
157
|
import { dispatchPersonaRoute } from './personas.js';
|
|
158
|
+
import { handleMe } from './operator.js';
|
|
159
|
+
import { handleOpsApi } from './ops-api.js';
|
|
157
160
|
import { handleRoleJudge } from './role-judge.js';
|
|
158
161
|
import { handleAudioStt } from './audio-stt.js';
|
|
159
162
|
import { handleLlmModels } from './llm-models.js';
|
|
160
163
|
import { handleLlmHostsConfig } from './llm-hosts-config.js';
|
|
161
164
|
import { handleLlmRotationGet, handleLlmRotationNext } from './llm-rotation.js';
|
|
165
|
+
import { handleSetupClaim } from './setup-claim.js';
|
|
162
166
|
import {
|
|
163
167
|
handleLlmProvidersList,
|
|
164
168
|
handleLlmProviderSet,
|
|
@@ -311,6 +315,7 @@ import {
|
|
|
311
315
|
handleVoiceTranscribe,
|
|
312
316
|
checkAuth,
|
|
313
317
|
bearerCredential,
|
|
318
|
+
setupBearerScopeDenied,
|
|
314
319
|
registerAuthPeerAddress,
|
|
315
320
|
type MetaApiOpts,
|
|
316
321
|
} from './meta-api.js';
|
|
@@ -385,6 +390,8 @@ import {
|
|
|
385
390
|
import { handleSessionTurnControl, SESSION_TURN_CONTROL_PATH } from '../../session/session-turn-control.js';
|
|
386
391
|
import { authenticatePodCredential, handlePodGrokCredential, handlePodGithubCredential, POD_CREDENTIAL_GROK_PATH, POD_CREDENTIAL_GITHUB_PATH } from './pod-credential-api.js';
|
|
387
392
|
import { handleOutboundReport } from './outbound-report.js';
|
|
393
|
+
import { handleErrorReportIngest } from './error-reports.js';
|
|
394
|
+
import { routeOutboundInProcess } from './outbound-report.js';
|
|
388
395
|
import { handleSelfEvent } from './self-event.js';
|
|
389
396
|
/** 매매 «실행»은 애드온이다(대표 09-20 결정 ③ · 별도 상용 저장소 · release/trading-export.yaml) — 공개 코어엔 없다.
|
|
390
397
|
* 있으면 그 경로를 켜고, 없으면 501. 경로를 변수로 둬 공개본 타입 검사가 «모듈 없음»으로 막히지 않게 한다. */
|
|
@@ -757,6 +764,8 @@ export function startNexusHttpServer(opts: NexusHttpServerOpts): NexusHttpServer
|
|
|
757
764
|
...(websocketHandler ? { websocket: websocketHandler } : {}),
|
|
758
765
|
} as unknown as Parameters<typeof Bun.serve>[0];
|
|
759
766
|
server = Bun.serve(fetchOpts);
|
|
767
|
+
// OB8 — this process now answers /v1/outbound itself; its own sendOutbound calls must not curl it.
|
|
768
|
+
setInProcessOutbound(routeOutboundInProcess);
|
|
760
769
|
// 데몬이 «실제로» 떴을 때만 재시작 복구(맡긴 일) — 주입된 러너(시험)는 건드리지 않는다.
|
|
761
770
|
if (!opts.execRequests) { try { execRequestRunner().reconcileInterrupted(); } catch { /* 복구 실패가 서버 기동을 막지 않는다 */ } }
|
|
762
771
|
resolvedPort = port;
|
|
@@ -781,6 +790,7 @@ export function startNexusHttpServer(opts: NexusHttpServerOpts): NexusHttpServer
|
|
|
781
790
|
url,
|
|
782
791
|
stop() {
|
|
783
792
|
try { server!.stop(true); } catch { /* idempotent */ }
|
|
793
|
+
setInProcessOutbound(null);
|
|
784
794
|
},
|
|
785
795
|
};
|
|
786
796
|
}
|
|
@@ -820,6 +830,9 @@ export async function routeRequest(
|
|
|
820
830
|
registerAuthPeerAddress(req, (server as BunServerLike & {
|
|
821
831
|
requestIP?: (request: Request) => { address: string } | null;
|
|
822
832
|
}).requestIP?.(req)?.address);
|
|
833
|
+
if (opts.metaApi && setupBearerScopeDenied(req, opts.metaApi)) {
|
|
834
|
+
return jsonResponse({ error: 'forbidden' }, 403);
|
|
835
|
+
}
|
|
823
836
|
|
|
824
837
|
// PR c — try WS upgrade before HTTP routing. On undefined return the
|
|
825
838
|
// bridge has accepted the upgrade (Bun completes the handshake) and
|
|
@@ -846,6 +859,12 @@ export async function routeRequest(
|
|
|
846
859
|
const { pathname } = url;
|
|
847
860
|
const method = req.method;
|
|
848
861
|
|
|
862
|
+
// Operator proxy credentials are distinct from the owner bearer. The ops handler
|
|
863
|
+
// performs its own operatorSignal gate before reading any run or checklist data.
|
|
864
|
+
if (pathname === '/v1/ops' || pathname.startsWith('/v1/ops/')) {
|
|
865
|
+
return handleOpsApi(req, opts.metaApi);
|
|
866
|
+
}
|
|
867
|
+
|
|
849
868
|
// Default-deny for every `/v1/` path that is not on PUBLIC_ROUTES.
|
|
850
869
|
// Reuses checkAuth (same-origin exemption + constant-time bearer).
|
|
851
870
|
// setupMode is not implemented on this server (P24b) — pass false.
|
|
@@ -876,7 +895,7 @@ export async function routeRequest(
|
|
|
876
895
|
const fallbackAuthorized = fallbackToken !== undefined && offered?.startsWith('Bearer ') === true
|
|
877
896
|
&& compareTokenConstTime(offered.slice('Bearer '.length).trim(), fallbackToken);
|
|
878
897
|
if (!(opts.metaApi ? checkAuth(req, opts.metaApi) : fallbackAuthorized)) {
|
|
879
|
-
const name = method === 'POST' && pathname === '/v1/tasks' && opts.metaApi
|
|
898
|
+
const name = method === 'POST' && (pathname === '/v1/tasks' || pathname === '/v1/reports/ingest') && opts.metaApi
|
|
880
899
|
? matchIngestAuthorization(req)?.name
|
|
881
900
|
: undefined;
|
|
882
901
|
if (name === undefined) {
|
|
@@ -1304,6 +1323,8 @@ export async function routeRequest(
|
|
|
1304
1323
|
if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
|
|
1305
1324
|
return handleTaskCreatePost(req, opts.metaApi);
|
|
1306
1325
|
}
|
|
1326
|
+
// ER2 — error reports forwarded by the bot VM (ingest token · same as /v1/tasks).
|
|
1327
|
+
if (method === 'POST' && pathname === '/v1/reports/ingest') return handleErrorReportIngest(req);
|
|
1307
1328
|
if (method === 'POST' && pathname.startsWith('/v1/tasks/') && pathname.endsWith('/approve')) {
|
|
1308
1329
|
if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
|
|
1309
1330
|
let taskId: string;
|
|
@@ -1512,6 +1533,10 @@ export async function routeRequest(
|
|
|
1512
1533
|
if (pathname === '/v1/llm/rotation' && method === 'OPTIONS') {
|
|
1513
1534
|
return handleLlmRotationGet(req);
|
|
1514
1535
|
}
|
|
1536
|
+
// The one-use link is verified by the claim handler, not by the owner bearer.
|
|
1537
|
+
if (pathname === '/v1/setup/claim' && (method === 'POST' || method === 'OPTIONS')) {
|
|
1538
|
+
return method === 'OPTIONS' ? corsPreflight('POST, OPTIONS') : handleSetupClaim(req);
|
|
1539
|
+
}
|
|
1515
1540
|
// PWA `/setup` wizard Phase 1 (2026-05-19) — LLM provider 첫 셋업.
|
|
1516
1541
|
// POST 는 mutation 블록 안에서 등록 (memory `feedback_post_route_must_be_in_method_block`).
|
|
1517
1542
|
// GET sibling 은 아래 GET-only 블록에 등록.
|
|
@@ -1969,6 +1994,18 @@ export async function routeRequest(
|
|
|
1969
1994
|
}
|
|
1970
1995
|
// HANDOFF §4.2 follow-up — skill-side mirror of the workflow
|
|
1971
1996
|
// router. Same regex → LLM cascade, applied to the skill index.
|
|
1997
|
+
// SK2 (PWA) — skills the index could not read: list ⊕ confirmed repair.
|
|
1998
|
+
if (pathname === '/v1/skills/problems' || pathname === '/v1/skills/problems/repair') {
|
|
1999
|
+
if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
|
|
2000
|
+
const { handleSkillProblems } = await import('./skill-problems.js');
|
|
2001
|
+
return handleSkillProblems(req, opts.metaApi);
|
|
2002
|
+
}
|
|
2003
|
+
// SC1 (PWA · beta) — «공유용 캡처»: masked screen PNG in, metadata chunks stripped, stored under <configDir>/captures.
|
|
2004
|
+
if (pathname === '/v1/captures' || pathname.startsWith('/v1/captures/')) {
|
|
2005
|
+
if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
|
|
2006
|
+
const { handleShareCaptures } = await import('./share-captures.js');
|
|
2007
|
+
return handleShareCaptures(req, opts.metaApi);
|
|
2008
|
+
}
|
|
1972
2009
|
if (pathname === '/v1/skills/route' && method === 'POST') {
|
|
1973
2010
|
if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
|
|
1974
2011
|
return handleSkillRoute(req, opts.metaApi);
|
|
@@ -2133,6 +2170,9 @@ export async function routeRequest(
|
|
|
2133
2170
|
}
|
|
2134
2171
|
return jsonResponse({ error: 'static-not-wired' }, 404);
|
|
2135
2172
|
}
|
|
2173
|
+
// OPS1·OPS2 — whether this caller is an operator (draws the ops area). Answers everyone before any auth gate;
|
|
2174
|
+
// false unless this daemon's config has operator.enabled.
|
|
2175
|
+
if (pathname === '/v1/me' && req.method === 'GET') return handleMe(req, opts.metaApi ?? ({} as MetaApiOpts));
|
|
2136
2176
|
if (pathname === '/v1/health') {
|
|
2137
2177
|
return handleHealth(opts.state, opts.registry, {
|
|
2138
2178
|
bindHost: bind.hostname,
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { randomUUID } from 'node:crypto';
|
|
18
18
|
import { matchTempToken } from '../../auth/temp-tokens.js';
|
|
19
|
+
import { matchSetupBearer } from '../../auth/setup-link-tokens.js';
|
|
19
20
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
20
21
|
import { extname } from 'node:path';
|
|
21
22
|
|
|
@@ -215,6 +216,8 @@ type AuthReason =
|
|
|
215
216
|
| 'untrusted-same-origin-peer'
|
|
216
217
|
| 'bearer-match'
|
|
217
218
|
| 'temp-token'
|
|
219
|
+
| 'setup-bearer'
|
|
220
|
+
| 'setup-scope-denied'
|
|
218
221
|
| 'no-bearer-configured'
|
|
219
222
|
| 'missing-auth-header'
|
|
220
223
|
| 'bearer-length-mismatch'
|
|
@@ -248,8 +251,27 @@ export function registerAuthPeerAddress(req: Request, peerAddress: string | unde
|
|
|
248
251
|
authPeerAddresses.set(req, peerAddress);
|
|
249
252
|
}
|
|
250
253
|
|
|
254
|
+
function setupRouteAllowed(req: Request): boolean {
|
|
255
|
+
const path = new URL(req.url).pathname;
|
|
256
|
+
if (path.startsWith('/v1/setup/')) return true;
|
|
257
|
+
return (req.method === 'POST' && path === '/v1/prompt/stream')
|
|
258
|
+
|| (req.method === 'GET' && (
|
|
259
|
+
/^\/v1\/sessions\/store\/[^/]+$/.test(path) || path === '/v1/chat/events'
|
|
260
|
+
));
|
|
261
|
+
}
|
|
262
|
+
|
|
251
263
|
function decideAuth(req: Request, opts: MetaApiOpts): { ok: boolean; reason: AuthReason } {
|
|
252
264
|
if (opts.noAuth) return { ok: true, reason: 'noauth' };
|
|
265
|
+
const offered = req.headers.get('authorization');
|
|
266
|
+
if (offered?.startsWith('Bearer elsb_')) {
|
|
267
|
+
const owner = decideBearer(req, opts);
|
|
268
|
+
if (owner.ok) return owner;
|
|
269
|
+
const bearer = offered.slice('Bearer '.length).trim();
|
|
270
|
+
if (!matchSetupBearer(bearer)) return { ok: false, reason: 'bearer-mismatch' };
|
|
271
|
+
return setupRouteAllowed(req)
|
|
272
|
+
? { ok: true, reason: 'setup-bearer' }
|
|
273
|
+
: { ok: false, reason: 'setup-scope-denied' };
|
|
274
|
+
}
|
|
253
275
|
const peerAddress = authPeerAddresses.get(req);
|
|
254
276
|
if (isSameOriginRequest(req, peerAddress)) return { ok: true, reason: 'same-origin' };
|
|
255
277
|
const peerRejected = req.headers.get('sec-fetch-site') === 'same-origin'
|
|
@@ -292,6 +314,10 @@ export function bearerCredential(req: Request, opts: MetaApiOpts): 'noauth' | 'b
|
|
|
292
314
|
// check — without duplicating security-sensitive code.
|
|
293
315
|
const authDecisions = new WeakMap<Request, { ok: boolean; reason: AuthReason }>();
|
|
294
316
|
|
|
317
|
+
export function setupBearerScopeDenied(req: Request, opts: MetaApiOpts): boolean {
|
|
318
|
+
return decideAuth(req, opts).reason === 'setup-scope-denied';
|
|
319
|
+
}
|
|
320
|
+
|
|
295
321
|
export function checkAuth(req: Request, opts: MetaApiOpts): boolean {
|
|
296
322
|
const decision = decideAuth(req, opts);
|
|
297
323
|
authDecisions.set(req, decision);
|
|
@@ -322,6 +348,7 @@ export function checkAuth(req: Request, opts: MetaApiOpts): boolean {
|
|
|
322
348
|
* intentionally collapse here to avoid exposing token-length information. */
|
|
323
349
|
function authFailureResponse(req: Request, opts: MetaApiOpts): Response {
|
|
324
350
|
const decision = authDecisions.get(req) ?? decideAuth(req, opts);
|
|
351
|
+
if (decision.reason === 'setup-scope-denied') return jsonResponse({ error: 'forbidden' }, 403);
|
|
325
352
|
return jsonResponse({ error: 'unauthorized', reason: publicAuthFailureReason(decision.reason) }, 401);
|
|
326
353
|
}
|
|
327
354
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// OPS1·OPS2 — who is an «operator» (sees the ops area). Off unless this daemon's config says `operator.enabled: true`
|
|
2
|
+
// (only our own machines); an external install never reports an operator, whatever header or token arrives.
|
|
3
|
+
// Design: 내부 문서 `DESIGN-pwa-menu-reorg-and-ops-area-2026-10-02` §2.
|
|
4
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
5
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
6
|
+
import { debug } from '../../debug/log.js';
|
|
7
|
+
import { getUserConfig } from '../../user-config.js';
|
|
8
|
+
import { checkAuth, type MetaApiOpts } from './meta-api.js';
|
|
9
|
+
|
|
10
|
+
/** Set by the op.elanous.ai proxy (Caddy) after Google sign-in, server side; the proxy strips any incoming copy. */
|
|
11
|
+
export const OPERATOR_HEADER = 'x-elanous-operator';
|
|
12
|
+
|
|
13
|
+
export type OperatorSource = 'op-proxy' | 'owner' | null;
|
|
14
|
+
export interface OperatorSignal { operator: boolean; operatorSource: OperatorSource }
|
|
15
|
+
export interface OperatorConfig { enabled: boolean; proxySecretFile?: string }
|
|
16
|
+
|
|
17
|
+
export function readOperatorConfig(raw: unknown = getUserConfig().raw?.operator): OperatorConfig {
|
|
18
|
+
const o = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw as Record<string, unknown> : {};
|
|
19
|
+
return {
|
|
20
|
+
enabled: o.enabled === true,
|
|
21
|
+
...(typeof o.proxySecretFile === 'string' && o.proxySecretFile.trim() ? { proxySecretFile: o.proxySecretFile.trim() } : {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The proxy secret, only from a regular file nobody but the owner can read (mode 600); anything else turns the header path off. */
|
|
26
|
+
export function readProxySecret(path: string | undefined): string | null {
|
|
27
|
+
if (!path) return null;
|
|
28
|
+
try {
|
|
29
|
+
const st = statSync(path);
|
|
30
|
+
if (!st.isFile() || (st.mode & 0o077) !== 0) return null;
|
|
31
|
+
const value = readFileSync(path, 'utf8').trim();
|
|
32
|
+
return value.length >= 16 ? value : null;
|
|
33
|
+
} catch { return null; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sameSecret(offered: string, secret: string): boolean {
|
|
37
|
+
const a = Buffer.from(offered);
|
|
38
|
+
const b = Buffer.from(secret);
|
|
39
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface OperatorDeps { config?: OperatorConfig; readSecret?: (path: string | undefined) => string | null; isOwner?: (req: Request) => boolean }
|
|
43
|
+
|
|
44
|
+
export function operatorSignal(req: Request, opts: MetaApiOpts, deps: OperatorDeps = {}): OperatorSignal {
|
|
45
|
+
const config = deps.config ?? readOperatorConfig();
|
|
46
|
+
if (!config.enabled) return { operator: false, operatorSource: null };
|
|
47
|
+
const offered = req.headers.get(OPERATOR_HEADER);
|
|
48
|
+
if (offered) {
|
|
49
|
+
const secret = (deps.readSecret ?? readProxySecret)(config.proxySecretFile);
|
|
50
|
+
if (secret && sameSecret(offered, secret)) return { operator: true, operatorSource: 'op-proxy' };
|
|
51
|
+
}
|
|
52
|
+
if ((deps.isOwner ?? ((r: Request) => checkAuth(r, opts)))(req)) return { operator: true, operatorSource: 'owner' };
|
|
53
|
+
return { operator: false, operatorSource: null };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** `GET /v1/me` — answers every caller (no auth needed: it only says whether to draw the ops area). */
|
|
57
|
+
export function handleMe(req: Request, opts: MetaApiOpts, deps: OperatorDeps = {}): Response {
|
|
58
|
+
const signal = operatorSignal(req, opts, deps);
|
|
59
|
+
debug.log('ops.api', 'me', { operator: signal.operator, source: signal.operatorSource });
|
|
60
|
+
return Response.json(signal, { headers: { 'cache-control': 'no-store' } });
|
|
61
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { debug, redactSecretText } from '../../debug/log.js';
|
|
4
|
+
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
5
|
+
import { listChecklist } from '../../release-loop/checklist.js';
|
|
6
|
+
import { jsonResponse } from './json-response.js';
|
|
7
|
+
import { operatorSignal } from './operator.js';
|
|
8
|
+
import { createSeatsCache, todayKst } from './ops-seats.js';
|
|
9
|
+
import { liveSeatsSources } from './ops-seats-sources.js';
|
|
10
|
+
import type { MetaApiOpts } from './meta-api.js';
|
|
11
|
+
|
|
12
|
+
const VERSION = /^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/;
|
|
13
|
+
const IDENTIFIER = /^[A-Za-z0-9-]+$/;
|
|
14
|
+
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
15
|
+
let seatsCache: ReturnType<typeof createSeatsCache> | null = null;
|
|
16
|
+
/** Tests swap the sources; production builds them lazily on first use. */
|
|
17
|
+
export function setSeatsCacheForTest(cache: ReturnType<typeof createSeatsCache> | null): void { seatsCache = cache; }
|
|
18
|
+
const LOG_PATH = /^\/v1\/ops\/release\/runs\/([^/]+)\/nodes\/([^/]+)\/log$/;
|
|
19
|
+
|
|
20
|
+
interface RunNode { nodeId: string; ok?: unknown; output?: unknown }
|
|
21
|
+
interface RunRecord {
|
|
22
|
+
status: string;
|
|
23
|
+
path: string[];
|
|
24
|
+
startedAt: string;
|
|
25
|
+
input?: unknown;
|
|
26
|
+
nodes: RunNode[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function recordAt(path: string): RunRecord | null {
|
|
30
|
+
try {
|
|
31
|
+
if (!lstatSync(path).isFile()) return null;
|
|
32
|
+
const value: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
33
|
+
if (!value || typeof value !== 'object') return null;
|
|
34
|
+
const run = value as Partial<RunRecord>;
|
|
35
|
+
if (typeof run.status !== 'string' || !Array.isArray(run.path)
|
|
36
|
+
|| typeof run.startedAt !== 'string' || !Array.isArray(run.nodes)) return null;
|
|
37
|
+
return run as RunRecord;
|
|
38
|
+
} catch { return null; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function outputText(output: unknown): string {
|
|
42
|
+
if (typeof output === 'string') return output;
|
|
43
|
+
if (output === undefined || output === null) return '';
|
|
44
|
+
return JSON.stringify(output) ?? '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function nodeSummary(output: unknown): string {
|
|
48
|
+
let value = output;
|
|
49
|
+
if (typeof value === 'string') {
|
|
50
|
+
const text = value;
|
|
51
|
+
try { value = JSON.parse(text) as unknown; }
|
|
52
|
+
catch {
|
|
53
|
+
const last = text.split('\n').map((line) => line.trim()).filter(Boolean).at(-1);
|
|
54
|
+
try { value = JSON.parse(last ?? '') as unknown; } catch { return ''; }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!value || typeof value !== 'object') return '';
|
|
58
|
+
const summary = (value as { summary?: unknown }).summary;
|
|
59
|
+
return typeof summary === 'string' ? redactSecretText(summary).slice(0, 300) : '';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function runVersion(input: unknown): string | null {
|
|
63
|
+
if (!input || typeof input !== 'object') return null;
|
|
64
|
+
const version = (input as { version?: unknown }).version;
|
|
65
|
+
return typeof version === 'string' && VERSION.test(version) ? version : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The sole data gate for every read-only /v1/ops endpoint. */
|
|
69
|
+
export function handleOpsApi(req: Request, metaApi: MetaApiOpts | undefined): Response | Promise<Response> {
|
|
70
|
+
const url = new URL(req.url);
|
|
71
|
+
const { pathname } = url;
|
|
72
|
+
if (req.method !== 'GET') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
73
|
+
|
|
74
|
+
const signal = metaApi ? operatorSignal(req, metaApi) : { operator: false, operatorSource: null };
|
|
75
|
+
if (!signal.operator) {
|
|
76
|
+
debug.log('ops.api', 'refused', { path: pathname, source: signal.operatorSource });
|
|
77
|
+
return jsonResponse({ error: 'forbidden' }, 403);
|
|
78
|
+
}
|
|
79
|
+
const served = (response: Response): Response => {
|
|
80
|
+
debug.log('ops.api', 'served', { path: pathname, source: signal.operatorSource });
|
|
81
|
+
response.headers.set('cache-control', 'no-store');
|
|
82
|
+
return response;
|
|
83
|
+
};
|
|
84
|
+
if (pathname === '/v1/ops/seats') {
|
|
85
|
+
const date = url.searchParams.get('date') ?? todayKst();
|
|
86
|
+
if (!DATE.test(date) || Number.isNaN(Date.parse(`${date}T00:00:00+09:00`))) return jsonResponse({ error: 'invalid-date' }, 400);
|
|
87
|
+
seatsCache ??= createSeatsCache(liveSeatsSources());
|
|
88
|
+
return seatsCache(date).then((board) => served(jsonResponse(board)), () => jsonResponse({ error: 'seats-unavailable' }, 500));
|
|
89
|
+
}
|
|
90
|
+
const version = url.searchParams.get('version');
|
|
91
|
+
if (pathname === '/v1/ops/checklist') {
|
|
92
|
+
if (!version || !VERSION.test(version)) return jsonResponse({ error: 'invalid-version' }, 400);
|
|
93
|
+
return served(jsonResponse(listChecklist(version)));
|
|
94
|
+
}
|
|
95
|
+
if (pathname === '/v1/ops/release/runs') {
|
|
96
|
+
if (version !== null && !VERSION.test(version)) return jsonResponse({ error: 'invalid-version' }, 400);
|
|
97
|
+
const dir = join(effectiveInstanceRoot(), 'graph-runs', 'release-loop');
|
|
98
|
+
let filenames: string[];
|
|
99
|
+
try {
|
|
100
|
+
filenames = readdirSync(dir, { withFileTypes: true })
|
|
101
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json')
|
|
102
|
+
&& IDENTIFIER.test(entry.name.slice(0, -5)))
|
|
103
|
+
.map((entry) => entry.name);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return served(jsonResponse([]));
|
|
107
|
+
return jsonResponse({ error: 'runs-unavailable' }, 500);
|
|
108
|
+
}
|
|
109
|
+
const runs = filenames.flatMap((filename) => {
|
|
110
|
+
const run = recordAt(join(dir, filename));
|
|
111
|
+
if (!run) return [];
|
|
112
|
+
const v = runVersion(run.input);
|
|
113
|
+
if (version !== null && v !== version) return [];
|
|
114
|
+
return [{
|
|
115
|
+
runId: redactSecretText(filename.slice(0, -5)), status: redactSecretText(run.status),
|
|
116
|
+
startedAt: redactSecretText(run.startedAt), version: v,
|
|
117
|
+
path: run.path.filter((step): step is string => typeof step === 'string').map(redactSecretText),
|
|
118
|
+
nodes: run.nodes.filter((node) => node && typeof node.nodeId === 'string').map((node) => ({
|
|
119
|
+
nodeId: redactSecretText(node.nodeId), ok: typeof node.ok === 'boolean' ? node.ok : null,
|
|
120
|
+
summary: nodeSummary(node.output),
|
|
121
|
+
})),
|
|
122
|
+
}];
|
|
123
|
+
}).sort((a, b) => b.startedAt.localeCompare(a.startedAt)).slice(0, 20);
|
|
124
|
+
return served(jsonResponse(runs));
|
|
125
|
+
}
|
|
126
|
+
// WHATWG URL normalizes a literal runs/../nodes/... before routing.
|
|
127
|
+
if (pathname.startsWith('/v1/ops/release/nodes/')) return jsonResponse({ error: 'invalid-id' }, 400);
|
|
128
|
+
if (pathname.startsWith('/v1/ops/release/runs/')) {
|
|
129
|
+
const match = LOG_PATH.exec(pathname);
|
|
130
|
+
if (!match) return jsonResponse({ error: 'invalid-id' }, 400);
|
|
131
|
+
const [, id, nodeId] = match;
|
|
132
|
+
if (!IDENTIFIER.test(id!) || !IDENTIFIER.test(nodeId!)) return jsonResponse({ error: 'invalid-id' }, 400);
|
|
133
|
+
const run = recordAt(join(effectiveInstanceRoot(), 'graph-runs', 'release-loop', `${id}.json`));
|
|
134
|
+
const node = run?.nodes.find((candidate) => candidate?.nodeId === nodeId);
|
|
135
|
+
if (!node || node.output === undefined || node.output === null) return jsonResponse({ error: 'not-found' }, 404);
|
|
136
|
+
return served(jsonResponse({ log: redactSecretText(outputText(node.output)).slice(-4_000) }));
|
|
137
|
+
}
|
|
138
|
+
return jsonResponse({ error: 'not-found' }, 404);
|
|
139
|
+
}
|