loki-mode 11.3.0 → 11.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -650
- package/SKILL.md +5 -5
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +2 -2
- package/autonomy/context-tracker.py +4 -4
- package/autonomy/council-v2.sh +1 -1
- package/autonomy/grill.sh +1 -1
- package/autonomy/lib/done-recognition.sh +1 -1
- package/autonomy/lib/efficiency_cost.py +8 -0
- package/autonomy/lib/engine10-push.sh +22 -8
- package/autonomy/lib/prd-enrich.sh +1 -1
- package/autonomy/lib/voter-agents.sh +1 -1
- package/autonomy/loki +161 -105
- package/autonomy/run.sh +30 -18
- package/autonomy/sandbox.sh +1 -1
- package/autonomy/tui.sh +6 -5
- package/autonomy/verify.sh +1 -1
- package/bin/loki +195 -59
- package/completions/_loki +8 -0
- package/completions/loki.bash +3 -3
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +47 -45
- package/docs/AGENT-CHANGE-RECEIPT.md +2 -2
- package/docs/ARCHITECTURE.md +337 -0
- package/docs/BRANDING.md +124 -0
- package/docs/CLI-REFERENCE.md +11 -1
- package/docs/COMPONENTS.md +447 -0
- package/docs/DEPLOYMENT.md +9 -9
- package/docs/DOCKER.md +469 -0
- package/docs/INSTALLATION.md +67 -1045
- package/docs/KNOWN-ISSUES.md +11 -35
- package/docs/OTEL-COLLECTOR.md +24 -0
- package/docs/ROUTING-EVIDENCE.md +32 -0
- package/docs/TESTING.md +161 -0
- package/docs/air-gapped.md +1 -2
- package/docs/alternative-installations.md +14 -24
- package/docs/architecture/DECISIONS.md +333 -0
- package/docs/audit-logging.md +7 -7
- package/docs/authentication.md +3 -3
- package/docs/authorization.md +1 -1
- package/docs/certification/04-production-deployment/lab.md +6 -6
- package/docs/certification/04-production-deployment/lesson.md +4 -4
- package/docs/certification/README.md +1 -1
- package/docs/control-plane-container.md +2 -2
- package/docs/dev/real-run.md +34 -0
- package/docs/dev/release-checklist.md +2 -2
- package/docs/environment-variables.md +1 -0
- package/docs/exit-codes.md +3 -3
- package/docs/git-workflow.md +3 -3
- package/docs/metrics.md +37 -55
- package/docs/network-security.md +1 -1
- package/docs/siem-integration.md +5 -5
- package/docs/spec-first-intent.md +28 -0
- package/docs/v10/CLI-MODERN.md +7 -3
- package/docs/v10/DECISIONS.md +59 -0
- package/docs/v10/DEPS.md +22 -22
- package/docs/v10/ENGINE.md +1 -1
- package/docs/v10/FAILURE-CLASSES.md +212 -1
- package/docs/v10/FOUNDER-QUEUE.md +1 -0
- package/docs/v10/GUARDS.md +31 -0
- package/docs/v10/GUIDE.md +7 -5
- package/docs/v10/LEGACY-REMOVAL.md +4 -6
- package/docs/v10/METRICS.md +14 -2
- package/docs/v10/MODERNIZE.md +1 -1
- package/docs/v10/OPERATING-MODEL.md +4 -3
- package/docs/v10/PROGRESS.md +35 -0
- package/docs/v11/COST-HALF.md +352 -0
- package/docs/v11/INTOTO-AGENT-CHANGE-PREDICATE.md +77 -0
- package/docs/v11/MARKET-SLICES.md +315 -0
- package/docs/v11/MCP-2026-07-28.md +55 -0
- package/docs/v11/REGISTRIES.md +730 -0
- package/docs/v11/VERIFY-PR-SANDBOX.md +41 -0
- package/loki-ts/data/model-pricing.json +52 -24
- package/loki-ts/dist/loki.js +1185 -945
- package/mcp/__init__.py +1 -1
- package/mcp/v10_tools.py +29 -0
- package/package.json +9 -11
- package/packages/control-plane/dist/ask-tools-server.js +8 -2
- package/packages/control-plane/dist/server.js +15832 -12361
- package/packages/control-plane/drizzle/0006_token_sessions.sql +1 -0
- package/packages/control-plane/drizzle/meta/0006_snapshot.json +807 -0
- package/packages/control-plane/drizzle/meta/_journal.json +7 -0
- package/packages/control-plane/src/db/schema.ts +1 -0
- package/packages/control-plane/src/server/routes/cost.ts +4 -3
- package/packages/control-plane/src/server/runs.ts +3 -3
- package/packages/control-plane/ui/dist/assets/index-DbLzr87u.js +190 -0
- package/packages/control-plane/ui/dist/index.html +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/opencode.sh +2 -3
- package/src/observability/genai-spans.js +104 -0
- package/src/observability/otel-bridge.js +49 -1
- package/src/observability/otel.js +2 -2
- package/src/protocols/mcp-client.js +6 -2
- package/src/protocols/mcp-server.js +191 -6
- package/src/protocols/transport/sse.js +23 -2
- package/tools/audit-docs.py +1 -1
- package/web-app/dist/assets/{AdminPage-D_e45uTw.js → AdminPage-DuRQZsF9.js} +1 -1
- package/web-app/dist/assets/{Avatar-T3nIpoaf.js → Avatar-7GtOQvsO.js} +1 -1
- package/web-app/dist/assets/{Badge-BKqb59pP.js → Badge-CYgTnIxj.js} +1 -1
- package/web-app/dist/assets/{Button-DjVlkh8_.js → Button-rYSyIu7X.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-DXJgLGmh.js → CockpitPage-9adANY4o.js} +1 -1
- package/web-app/dist/assets/{ComparePage-BRP_eVc_.js → ComparePage-DEG2oA-R.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-CLjpGLjC.js → ErrorBoundary-CwtvY4xs.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-DX-vb4Xy.js → EvidenceReceiptPanel-DTT73pPB.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-214HVHtX.js → GitHubIssuesPanel-CpWZLz2x.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-BEiZKc4t.js → GitHubPRsPanel-H293R7E2.js} +1 -1
- package/web-app/dist/assets/{HomePage-VvMHS5ga.js → HomePage-B11Imp9k.js} +2 -2
- package/web-app/dist/assets/{LoginPage-5WD-v7n3.js → LoginPage-BZBgx5Jr.js} +1 -1
- package/web-app/dist/assets/{MagicPage-DUQT_Jye.js → MagicPage-B27S4_X8.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-lga-z79v.js → MetricsPage-CfbJWNDG.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-CGhBX3G1.js → NotFoundPage-Tp_4zMSm.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-CdzF2GzZ.js → ProjectPage-CM4fgMsV.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-DRG64N8s.js → ProjectsPage-DP2Fvsrd.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-5fzW9Frb.js → SettingsPage-C4G9-EAy.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-DrTdxO4y.js → ShowcasePage-Dj-n4dwu.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-C9Mqs6Rd.js → SystemSettingsPage-BqMLpuGt.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-CUGPdSWo.js → TeamsPage-mIH8H96m.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-CNraDY0F.js → TemplatesPage-BagW-v4n.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-BIu_6Pea.js → TerminalOutput-CTQRHhnK.js} +1 -1
- package/web-app/dist/assets/{activity-Bk-kgqaf.js → activity-DNQiBDDL.js} +1 -1
- package/web-app/dist/assets/{bell-BXmUdmo8.js → bell-Coiwdad1.js} +1 -1
- package/web-app/dist/assets/{bot-CiuzXxUR.js → bot-Cou-rTgR.js} +1 -1
- package/web-app/dist/assets/{check-BEzjXppd.js → check-CsF-XtQp.js} +1 -1
- package/web-app/dist/assets/{chevron-left-AYWCXHf8.js → chevron-left-BJiy55Ht.js} +1 -1
- package/web-app/dist/assets/{circle-alert-BNg2uSRq.js → circle-alert-D6-pQQWq.js} +1 -1
- package/web-app/dist/assets/{clock-DTj0kkGT.js → clock-BOGqwgt9.js} +1 -1
- package/web-app/dist/assets/{cloud-x4nTDBLT.js → cloud-DsxhTtf5.js} +1 -1
- package/web-app/dist/assets/{code-xml-BVh_MLrk.js → code-xml-CxJDHUlE.js} +1 -1
- package/web-app/dist/assets/{database-DdfBaowG.js → database-BJePscl_.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-XuYFmAOY.js → dollar-sign-6qWGAkZo.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-D4cRnmeL.js → file-code-corner-DcOtYeDb.js} +1 -1
- package/web-app/dist/assets/{file-plus-CyoVlHc_.js → file-plus-WuRyLFo0.js} +1 -1
- package/web-app/dist/assets/{globe-Y6IK2RPW.js → globe-r-xCP4DE.js} +1 -1
- package/web-app/dist/assets/{hammer-BKzr9VZI.js → hammer-DTt0SsCB.js} +1 -1
- package/web-app/dist/assets/{index-B20cSx5G.js → index-BkbXyH8y.js} +3 -3
- package/web-app/dist/assets/{layers-DebNOhGS.js → layers-BvZhvSMY.js} +1 -1
- package/web-app/dist/assets/{loader-circle-DyGbrEgd.js → loader-circle-3gosTei9.js} +1 -1
- package/web-app/dist/assets/{lock-BKPpyt20.js → lock-CZZMxP0U.js} +1 -1
- package/web-app/dist/assets/{package-egF5Wi4y.js → package-C5A8CkJr.js} +1 -1
- package/web-app/dist/assets/{plus-OiEgKIc-.js → plus-Btv7tTIF.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-CzKvrONq.js → refresh-cw-DsWLXgs_.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-Bu2mII0p.js → rotate-ccw-DOVDjway.js} +1 -1
- package/web-app/dist/assets/{scroll-text-CSkiyax8.js → scroll-text-smySzlrX.js} +1 -1
- package/web-app/dist/assets/{server-DlPthJFw.js → server-b5PHsqAU.js} +1 -1
- package/web-app/dist/assets/{shield-alert-CbpHWQQX.js → shield-alert-BD7FDlh-.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-DgGpZe-Q.js → thumbs-up-CAsFzbyE.js} +1 -1
- package/web-app/dist/assets/{trash-2-BV3GlCB-.js → trash-2-BJ35qHgl.js} +1 -1
- package/web-app/dist/assets/{trending-down-Y4TJtXhD.js → trending-down-Ozrd26cZ.js} +1 -1
- package/web-app/dist/assets/{trending-up-DsDNoHZY.js → trending-up-BhnMcM32.js} +1 -1
- package/web-app/dist/assets/{upload-gkThrc_I.js → upload-JVHfdwoA.js} +1 -1
- package/web-app/dist/assets/{usePolling-jpNSeNAe.js → usePolling-CTzbDV-e.js} +1 -1
- package/web-app/dist/assets/{user-3Zez1WOK.js → user-Bks3-GYv.js} +1 -1
- package/web-app/dist/index.html +1 -1
- package/docs/ARCHITECTURE-OVERVIEW.md +0 -209
- package/docs/AUTONOMI-ECOSYSTEM.md +0 -107
- package/docs/COCKPIT-SPEC.md +0 -72
- package/docs/COCKPIT-UX-MANDATE.md +0 -53
- package/docs/COMPETITIVE-ANALYSIS.md +0 -303
- package/docs/COMPETITIVE-NEXT-10.md +0 -316
- package/docs/COMPETITIVE-RALPH-ZEROSHOT-2026-07-08.md +0 -81
- package/docs/DASHBOARD-9.12-EVIDENCE.md +0 -97
- package/docs/DASHBOARD-ARCHITECTURE.md +0 -425
- package/docs/DASHBOARD_V2_CHECKLIST.md +0 -243
- package/docs/DEMOS.md +0 -63
- package/docs/ENTERPRISE-IDENTITY-ROADMAP.md +0 -206
- package/docs/EVIDENCE-RECEIPT-BUILD-APPLICABILITY.md +0 -79
- package/docs/FEAT-PRDREUSE-DOCKER-PLAN.md +0 -144
- package/docs/GATE-FAILURE-TRIAGE.md +0 -254
- package/docs/HANDOFF-2026-08-03.md +0 -439
- package/docs/LOOP-CANDIDATE-PROPOSAL-v1.md +0 -167
- package/docs/LOOP-HARNESS-AUDIT.md +0 -684
- package/docs/MERGE-DEDUP-MAP.md +0 -420
- package/docs/MERGE-ROUTE-MAP.md +0 -139
- package/docs/MODEL-REGISTRY-PLAN.md +0 -155
- package/docs/OUTCOME-FRONTIER.md +0 -536
- package/docs/P2-SPEC-ROBUSTNESS-PLAN.md +0 -192
- package/docs/PLANS-INDEX.md +0 -32
- package/docs/PRD-REUSE-DONE-RECOGNITION-PLAN.md +0 -536
- package/docs/PROMPT-ABLATION-RESULT.md +0 -97
- package/docs/R4-TRUST-TRAJECTORY-DESIGN.md +0 -127
- package/docs/R5-AUTO-WIKI-DESIGN.md +0 -137
- package/docs/R6-ROLLBACK-CHECKPOINT-PLAN.md +0 -107
- package/docs/R7-ZERO-CONFIG-FIRST-RUN-PLAN.md +0 -137
- package/docs/R8-SHAREABLE-TEAM-ASSETS-PLAN.md +0 -129
- package/docs/R9-OPEN-CORE-HOOKS-PLAN.md +0 -113
- package/docs/RARV-C-100X-PLAN.md +0 -286
- package/docs/RARV-C-LOOP-EFFICIENCY-PLAN.md +0 -470
- package/docs/SHAREABLE-PROOF-PLAN.md +0 -194
- package/docs/SONNET5-DEFAULT-PLAN.md +0 -157
- package/docs/SPEED-AND-FIRST-PASS-PLAN.md +0 -281
- package/docs/STALE-STATE-AUDIT.md +0 -174
- package/docs/SYNERGY-TASKS.md +0 -143
- package/docs/TASKLIST-ACCURACY-PLAN.md +0 -355
- package/docs/TOP-100-BACKLOG.md +0 -159
- package/docs/V8-ACCEPTANCE-TRIAGE-2026-07-24.md +0 -114
- package/docs/V8-COMPLEXITY-AUDIT-2026-07-24.md +0 -45
- package/docs/V8-MAJOR-RELEASE-PLAN.md +0 -137
- package/docs/V8-OVERNIGHT-PLAN-2026-07-25.md +0 -82
- package/docs/V8-SDK-RESEARCH-RAW.md +0 -129
- package/docs/WELCOME-OPENER-PLAN.md +0 -106
- package/docs/adoption-baseline-2026-07-31.md +0 -84
- package/docs/architecture/bmad-integration-epic.md +0 -271
- package/docs/architecture/bmad-integration-review.md +0 -86
- package/docs/architecture/bmad-integration-validation.md +0 -249
- package/docs/architecture/bmad-loki-voice-agent-council-analysis.md +0 -61
- package/docs/auto-claude-comparison.md +0 -250
- package/docs/bug-fixes/agent-01-cli-fixes.md +0 -101
- package/docs/bug-fixes/agent-02-purplelab-fixes.md +0 -88
- package/docs/bug-fixes/agent-03-dashboard-fixes.md +0 -119
- package/docs/bug-fixes/agent-04-memory-fixes.md +0 -105
- package/docs/bug-fixes/agent-05-provider-fixes.md +0 -86
- package/docs/bug-fixes/agent-06-integration-fixes.md +0 -101
- package/docs/bug-fixes/agent-07-dash-run-fixes.md +0 -101
- package/docs/bug-fixes/agent-08-docker-fixes.md +0 -164
- package/docs/bug-fixes/agent-09-e2e-build-fixes.md +0 -69
- package/docs/bug-fixes/agent-10-e2e-fullstack-fixes.md +0 -102
- package/docs/bug-fixes/agent-11-e2e-session-fixes.md +0 -70
- package/docs/bug-fixes/agent-12-scenario-fixes.md +0 -120
- package/docs/bug-fixes/agent-13-enterprise-fixes.md +0 -143
- package/docs/bug-fixes/agent-14-uat-newuser-fixes.md +0 -88
- package/docs/bug-fixes/agent-15-uat-poweruser-fixes.md +0 -132
- package/docs/bug-fixes/agent-19-code-review.md +0 -316
- package/docs/bug-fixes/agent-20-architecture-review.md +0 -331
- package/docs/competitive/bolt-new-analysis.md +0 -579
- package/docs/competitive/emergence-others-analysis.md +0 -605
- package/docs/competitive/replit-lovable-analysis.md +0 -622
- package/docs/dashboard-guide.md +0 -487
- package/docs/enterprise/migration.md +0 -265
- package/docs/history/one-run-audit.md +0 -304
- package/docs/plans/FORGE-AUTONOMOUS-QUEUE.md +0 -312
- package/docs/plans/ULTRAPLAN-FORGE-BAAS.md +0 -612
- package/docs/plans/UT2-6-LSP-DIAGNOSTIC-BROADCAST.md +0 -394
- package/docs/prd-purple-lab-ide.md +0 -152
- package/docs/prd-purple-lab-platform-v2.md +0 -223
- package/docs/prd-purple-lab-platform.md +0 -319
- package/docs/retrospectives/v7.5.15-fleet-postmortem.md +0 -325
- package/docs/retrospectives/v7.5.15-honesty-audit.md +0 -136
- package/docs/retrospectives/v7.5.15-llm-failure-modes.md +0 -49
- package/docs/show-hn-post.md +0 -46
- package/docs/test-scenarios/enterprise-scenarios.md +0 -732
- package/docs/thick2thin.md +0 -173
- package/packages/control-plane/ui/dist/assets/index-DcBJXjCJ.js +0 -190
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Loki Mode</title>
|
|
7
|
-
<script type="module" crossorigin src="./assets/index-
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-DbLzr87u.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="./assets/index-CxZWgwie.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "loki-mode",
|
|
4
4
|
"displayName": "Loki Mode",
|
|
5
|
-
"version": "11.3.
|
|
5
|
+
"version": "11.3.2",
|
|
6
6
|
"description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Autonomi",
|
package/providers/opencode.sh
CHANGED
|
@@ -21,9 +21,8 @@
|
|
|
21
21
|
# v0.86.0 from 2025-08-09, and Roo Code is archived, Continue read-only --
|
|
22
22
|
# so opencode, not aider, is the durable cheap-model route.)
|
|
23
23
|
#
|
|
24
|
-
# Cost context
|
|
25
|
-
# MiniMax M2.5
|
|
26
|
-
# $275.76 -- an open model matching frontier accuracy at ~7.5x lower cost.
|
|
24
|
+
# Cost context: see docs/ROUTING-EVIDENCE.md (measurement source, date and
|
|
25
|
+
# caveats for the MiniMax M2.5 vs Claude Opus 4.6 cost-parity figures).
|
|
27
26
|
#
|
|
28
27
|
# CLI SURFACE (verified against the installed binary, v1.17.9, not assumed):
|
|
29
28
|
# opencode run [message..] non-interactive run
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure mapper: engine10 events.jsonl envelopes -> gen_ai span descriptors.
|
|
5
|
+
* No I/O, no network, no clock. Attributes are built from a fixed whitelist of
|
|
6
|
+
* event fields, so prompt or completion content and secrets can never reach a span.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// OpenTelemetry GenAI semantic conventions version these attribute names follow.
|
|
10
|
+
const GENAI_SEMCONV_VERSION = '1.37.0';
|
|
11
|
+
|
|
12
|
+
const AGENT_NAME = 'loki';
|
|
13
|
+
|
|
14
|
+
function toNs(ts) {
|
|
15
|
+
const ms = Date.parse(ts);
|
|
16
|
+
return Number.isFinite(ms) ? BigInt(ms) * 1000000n : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isNum(x) {
|
|
20
|
+
return typeof x === 'number' && Number.isFinite(x);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Unknown usage is absent, never 0 (mirrors engine10/cost.ts): an unmetered or
|
|
24
|
+
// unmeasured cost event, or an all-zero usage record, carries no token attributes.
|
|
25
|
+
function usageAttrs(d) {
|
|
26
|
+
if (d.source === 'cli-invoker-unmetered' || d.source === 'not measured') return {};
|
|
27
|
+
if (!isNum(d.input_tokens) || !isNum(d.output_tokens)) return {};
|
|
28
|
+
if (d.input_tokens === 0 && d.output_tokens === 0) return {};
|
|
29
|
+
return { 'gen_ai.usage.input_tokens': d.input_tokens, 'gen_ai.usage.output_tokens': d.output_tokens };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {Array<{seq:number,ts:string,run:string,type:string,stage:string|null,data:object}>} events
|
|
34
|
+
* @returns {Array<{key:string,parentKey:string|null,name:string,startNs:bigint,endNs:bigint,status:'ok'|'error'|'unset',attributes:object}>}
|
|
35
|
+
*/
|
|
36
|
+
function mapRunEvents(events) {
|
|
37
|
+
const list = (Array.isArray(events) ? events : []).filter((e) => e && typeof e.type === 'string' && toNs(e.ts) !== null);
|
|
38
|
+
if (list.length === 0) return [];
|
|
39
|
+
const started = list.find((e) => e.type === 'run.started');
|
|
40
|
+
const root = started || list[0];
|
|
41
|
+
const last = list[list.length - 1];
|
|
42
|
+
const done = list.find((e) => e.type === 'run.completed');
|
|
43
|
+
const provider = started && typeof started.data.provider === 'string' ? started.data.provider : null;
|
|
44
|
+
const runModel = started && typeof started.data.model === 'string' ? started.data.model : null;
|
|
45
|
+
|
|
46
|
+
const base = { 'gen_ai.agent.name': AGENT_NAME };
|
|
47
|
+
if (provider) base['gen_ai.provider.name'] = provider;
|
|
48
|
+
const rootKey = 'run:' + root.run;
|
|
49
|
+
const spans = [{
|
|
50
|
+
key: rootKey, parentKey: null, name: 'invoke_agent ' + AGENT_NAME,
|
|
51
|
+
startNs: toNs(root.ts), endNs: toNs((done || last).ts),
|
|
52
|
+
status: done ? (done.data.verdict === 'FAILED' ? 'error' : 'ok') : 'unset',
|
|
53
|
+
attributes: Object.assign({ 'gen_ai.operation.name': 'invoke_agent' }, base, runModel ? { 'gen_ai.request.model': runModel } : {}),
|
|
54
|
+
}];
|
|
55
|
+
|
|
56
|
+
const open = new Map();
|
|
57
|
+
const costs = [];
|
|
58
|
+
for (const e of list) {
|
|
59
|
+
const d = e.data || {};
|
|
60
|
+
if (e.type === 'stage.started' && e.stage) {
|
|
61
|
+
open.set(e.stage, e);
|
|
62
|
+
} else if ((e.type === 'stage.completed' || e.type === 'stage.failed' || e.type === 'stage.skipped') && e.stage) {
|
|
63
|
+
const s = open.get(e.stage);
|
|
64
|
+
open.delete(e.stage);
|
|
65
|
+
spans.push({
|
|
66
|
+
key: 'stage:' + e.stage, parentKey: rootKey, name: 'stage ' + e.stage,
|
|
67
|
+
startNs: toNs((s || e).ts), endNs: toNs(e.ts), status: e.type === 'stage.failed' ? 'error' : 'ok',
|
|
68
|
+
attributes: { 'gen_ai.operation.name': 'invoke_agent', 'loki.stage': e.stage },
|
|
69
|
+
});
|
|
70
|
+
} else if (e.type === 'cost') {
|
|
71
|
+
costs.push(e);
|
|
72
|
+
} else if (e.type.indexOf('tool.') === 0 && typeof d.name === 'string' && d.name) {
|
|
73
|
+
const t = toNs(e.ts);
|
|
74
|
+
spans.push({
|
|
75
|
+
key: 'tool:' + e.seq, parentKey: e.stage ? 'stage:' + e.stage : rootKey, name: 'execute_tool ' + d.name,
|
|
76
|
+
startNs: t, endNs: t, status: e.type === 'tool.failed' ? 'error' : 'ok',
|
|
77
|
+
attributes: { 'gen_ai.operation.name': 'execute_tool', 'gen_ai.tool.name': d.name },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Stages still open at the end of the log end at the last event.
|
|
82
|
+
for (const [stage, s] of open) {
|
|
83
|
+
spans.push({
|
|
84
|
+
key: 'stage:' + stage, parentKey: rootKey, name: 'stage ' + stage,
|
|
85
|
+
startNs: toNs(s.ts), endNs: toNs(last.ts), status: 'unset',
|
|
86
|
+
attributes: { 'gen_ai.operation.name': 'invoke_agent', 'loki.stage': stage },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
for (const e of costs) {
|
|
90
|
+
const attrs = usageAttrs(e.data || {});
|
|
91
|
+
const stageSpan = spans.find((s) => s.key === 'stage:' + e.stage);
|
|
92
|
+
const targets = stageSpan ? [stageSpan, spans[0]] : [spans[0]];
|
|
93
|
+
for (const t of targets) {
|
|
94
|
+
if (t === spans[0] && typeof e.data.model === 'string' && e.data.model) t.attributes['gen_ai.request.model'] = e.data.model;
|
|
95
|
+
for (const [k, v] of Object.entries(attrs)) t.attributes[k] = (t.attributes[k] || 0) + v;
|
|
96
|
+
}
|
|
97
|
+
if (stageSpan && typeof e.data.model === 'string' && e.data.model) stageSpan.attributes['gen_ai.request.model'] = e.data.model;
|
|
98
|
+
}
|
|
99
|
+
// Parents before children so a replaying exporter can link them.
|
|
100
|
+
const rank = (x) => (x.parentKey === null ? 0 : x.key.indexOf('stage:') === 0 ? 1 : 2);
|
|
101
|
+
return spans.map((x, i) => ({ x, i })).sort((p, q) => rank(p.x) - rank(q.x) || p.i - q.i).map((o) => o.x);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { GENAI_SEMCONV_VERSION, mapRunEvents };
|
|
@@ -195,6 +195,52 @@ function processEventFile(filepath, tracer, traceId, otelRef) {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Engine10 gen_ai spans (LOKI_OTEL_GENAI=1): completed runs under .loki/runs/<id>/events.jsonl
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
const exportedRuns = new Set();
|
|
203
|
+
|
|
204
|
+
function genaiEnabled() {
|
|
205
|
+
return /^(1|true|yes|on)$/i.test(String(process.env.LOKI_OTEL_GENAI || '').trim());
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Emit the span tree for one run's envelopes. Returns the number of spans ended. */
|
|
209
|
+
function exportEngine10Run(events, tracer, traceId, otelRef, rootParentSpanId) {
|
|
210
|
+
const spans = require('./genai-spans').mapRunEvents(events);
|
|
211
|
+
const live = new Map();
|
|
212
|
+
for (const d of spans) {
|
|
213
|
+
const parent = d.parentKey ? live.get(d.parentKey) : null;
|
|
214
|
+
const span = tracer.startSpan(d.name, { traceId: traceId, parentSpanId: parent ? parent.spanId : (d.parentKey ? undefined : rootParentSpanId), attributes: Object.assign({}, d.attributes) });
|
|
215
|
+
span.startTimeUnixNano = String(d.startNs);
|
|
216
|
+
live.set(d.key, span);
|
|
217
|
+
if (d.status === 'ok') span.setStatus(otelRef.SpanStatusCode.OK);
|
|
218
|
+
else if (d.status === 'error') span.setStatus(otelRef.SpanStatusCode.ERROR);
|
|
219
|
+
span.end(d.endNs);
|
|
220
|
+
}
|
|
221
|
+
return spans.length;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function scanEngine10Runs(tracer, traceId, otelRef) {
|
|
225
|
+
if (!genaiEnabled()) return;
|
|
226
|
+
const runsDir = path.join(process.cwd(), lokiDir, 'runs');
|
|
227
|
+
try {
|
|
228
|
+
for (const id of fs.readdirSync(runsDir)) {
|
|
229
|
+
if (exportedRuns.has(id)) continue;
|
|
230
|
+
const f = path.join(runsDir, id, 'events.jsonl');
|
|
231
|
+
if (!fs.existsSync(f)) continue;
|
|
232
|
+
const events = [];
|
|
233
|
+
for (const line of fs.readFileSync(f, 'utf8').split('\n')) {
|
|
234
|
+
try { if (line.trim()) events.push(JSON.parse(line)); } catch (e) { /* torn line */ }
|
|
235
|
+
}
|
|
236
|
+
if (!events.some((e) => e && e.type === 'run.completed')) continue; // export complete trees only
|
|
237
|
+
exportedRuns.add(id);
|
|
238
|
+
const envParent = /^[0-9a-f]{16}$/.test(process.env.LOKI_PARENT_SPAN_ID || '') ? process.env.LOKI_PARENT_SPAN_ID : undefined; // MCP-D: parent from an MCP task's traceparent
|
|
239
|
+
exportEngine10Run(events, tracer, traceId, otelRef, envParent);
|
|
240
|
+
}
|
|
241
|
+
} catch (e) { /* no runs dir */ }
|
|
242
|
+
}
|
|
243
|
+
|
|
198
244
|
// ---------------------------------------------------------------------------
|
|
199
245
|
// Polling loop
|
|
200
246
|
// ---------------------------------------------------------------------------
|
|
@@ -229,7 +275,7 @@ function start() {
|
|
|
229
275
|
|
|
230
276
|
// Bound versions that close over initialized dependencies
|
|
231
277
|
const boundProcessEventFile = (filepath) => processEventFile(filepath, tracer, traceId, otelMod);
|
|
232
|
-
const boundScanPendingEvents = () => scanPendingEvents(tracer, traceId, otelMod);
|
|
278
|
+
const boundScanPendingEvents = () => { scanPendingEvents(tracer, traceId, otelMod); scanEngine10Runs(tracer, traceId, otelMod); };
|
|
233
279
|
|
|
234
280
|
pollInterval = setInterval(boundScanPendingEvents, POLL_INTERVAL_MS);
|
|
235
281
|
|
|
@@ -275,6 +321,8 @@ function start() {
|
|
|
275
321
|
module.exports = {
|
|
276
322
|
start,
|
|
277
323
|
processEventFile,
|
|
324
|
+
exportEngine10Run,
|
|
325
|
+
scanEngine10Runs,
|
|
278
326
|
activeSpans,
|
|
279
327
|
_getLastProcessedFile: function() { return lastProcessedFile; },
|
|
280
328
|
_resetState: function() { lastProcessedFile = ''; },
|
|
@@ -95,10 +95,10 @@ class Span {
|
|
|
95
95
|
return this;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
end() {
|
|
98
|
+
end(endNanos) {
|
|
99
99
|
if (this._ended) return;
|
|
100
100
|
this._ended = true;
|
|
101
|
-
this.endTimeUnixNano = nowNanos();
|
|
101
|
+
this.endTimeUnixNano = endNanos !== undefined ? String(endNanos) : nowNanos(); // endNanos: replayed spans (gen_ai bridge) keep event time
|
|
102
102
|
// Register with the exporter
|
|
103
103
|
if (_activeExporter) {
|
|
104
104
|
_activeExporter.addSpan(this);
|
|
@@ -70,6 +70,7 @@ class MCPClient extends EventEmitter {
|
|
|
70
70
|
this._connected = false;
|
|
71
71
|
this._connectingPromise = null;
|
|
72
72
|
this._serverInfo = null;
|
|
73
|
+
this._protocolVersion = null;
|
|
73
74
|
this._serverCapabilities = null;
|
|
74
75
|
this._tools = null;
|
|
75
76
|
|
|
@@ -84,6 +85,7 @@ class MCPClient extends EventEmitter {
|
|
|
84
85
|
get name() { return this._name; }
|
|
85
86
|
get connected() { return this._connected; }
|
|
86
87
|
get serverInfo() { return this._serverInfo; }
|
|
88
|
+
get protocolVersion() { return this._protocolVersion; }
|
|
87
89
|
get tools() { return this._tools; }
|
|
88
90
|
|
|
89
91
|
async connect() {
|
|
@@ -102,13 +104,14 @@ class MCPClient extends EventEmitter {
|
|
|
102
104
|
|
|
103
105
|
const initResult = await this._sendRequest('initialize', {
|
|
104
106
|
clientInfo: { name: 'loki-mode-client', version: '1.0.0' },
|
|
105
|
-
protocolVersion: '
|
|
107
|
+
protocolVersion: '2025-11-25'
|
|
106
108
|
});
|
|
107
109
|
|
|
108
110
|
this._serverInfo = initResult.serverInfo || null;
|
|
111
|
+
this._protocolVersion = initResult.protocolVersion || null;
|
|
109
112
|
this._serverCapabilities = initResult.capabilities || null;
|
|
110
113
|
|
|
111
|
-
this._sendNotification('initialized', {});
|
|
114
|
+
this._sendNotification('notifications/initialized', {});
|
|
112
115
|
|
|
113
116
|
const toolsResult = await this._sendRequest('tools/list', {});
|
|
114
117
|
this._tools = toolsResult.tools || [];
|
|
@@ -142,6 +145,7 @@ class MCPClient extends EventEmitter {
|
|
|
142
145
|
this._connected = false;
|
|
143
146
|
this._tools = null;
|
|
144
147
|
this._serverInfo = null;
|
|
148
|
+
this._protocolVersion = null;
|
|
145
149
|
|
|
146
150
|
for (const [, pending] of this._pendingRequests) {
|
|
147
151
|
clearTimeout(pending.timer);
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
const path = require('path');
|
|
22
22
|
const fs = require('fs');
|
|
23
|
+
const crypto = require('crypto');
|
|
24
|
+
const { spawn } = require('child_process');
|
|
23
25
|
|
|
24
26
|
// ---------------------------------------------------------------------------
|
|
25
27
|
// Lazy tool/resource loading
|
|
@@ -93,11 +95,158 @@ function getServerInfo() {
|
|
|
93
95
|
|
|
94
96
|
return {
|
|
95
97
|
name: 'loki-mode',
|
|
96
|
-
version: version
|
|
97
|
-
protocolVersion: '2024-11-05'
|
|
98
|
+
version: version
|
|
98
99
|
};
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
// Oldest to newest; the last entry is the fallback offered to unknown clients.
|
|
103
|
+
const SUPPORTED_PROTOCOL_VERSIONS = ['2024-11-05', '2025-03-26', '2025-06-18', '2025-11-25'];
|
|
104
|
+
|
|
105
|
+
// MCP-C: 2026-07-28 (stateless profile) is advertised only with LOKI_MCP_2026_07=1. It is never the initialize fallback,
|
|
106
|
+
// because that profile removes the handshake and a legacy client must keep getting a stateful version.
|
|
107
|
+
const STATELESS_VERSION = '2026-07-28';
|
|
108
|
+
const META_PROTOCOL_VERSION = 'io.modelcontextprotocol/protocolVersion';
|
|
109
|
+
|
|
110
|
+
function advertisedProtocolVersions() {
|
|
111
|
+
return process.env.LOKI_MCP_2026_07 === '1' ? SUPPORTED_PROTOCOL_VERSIONS.concat(STATELESS_VERSION) : SUPPORTED_PROTOCOL_VERSIONS.slice();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function negotiateProtocolVersion(params) {
|
|
115
|
+
const requested = params && typeof params.protocolVersion === 'string' ? params.protocolVersion : null;
|
|
116
|
+
if (requested && advertisedProtocolVersions().includes(requested)) return requested;
|
|
117
|
+
return SUPPORTED_PROTOCOL_VERSIONS[SUPPORTED_PROTOCOL_VERSIONS.length - 1];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// MCP-D: Tasks extension (io.modelcontextprotocol/tasks), LOKI_MCP_TASKS=1
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// A task wraps exactly two existing tools, loki_v10_verify and loki_v10_run, by running the read-only adapter in
|
|
124
|
+
// mcp/v10_tools.py (argument list, no shell). It never computes or edits a verdict: the task result is the adapter's
|
|
125
|
+
// stdout, byte for byte. Cancel kills only the child this server spawned, by its recorded pid. State is in memory.
|
|
126
|
+
|
|
127
|
+
const TASK_TOOLS = {
|
|
128
|
+
loki_v10_verify: { cmd: 'verify', keys: ['receipt_path', 'repo_path'], required: [['receipt_path', 'repo_path']] },
|
|
129
|
+
loki_v10_run: { cmd: 'run', keys: ['ref', 'repo_path'], required: [['ref'], ['repo_path']] }
|
|
130
|
+
};
|
|
131
|
+
const TASK_MAX = 64;
|
|
132
|
+
const TASK_TIMEOUT_MS = 300000;
|
|
133
|
+
const TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/;
|
|
134
|
+
// Same list the adapter's _env() drops for the direct tool (mcp/v10_tools.py _DROPPED_ENV): one mechanism, so a task
|
|
135
|
+
// sees the same environment as the direct call. MCP_AUTH_TOKEN is the server's own bearer and is never forwarded.
|
|
136
|
+
const TASK_DROPPED_ENV = ['LOKI_CONTROL_TOKEN', 'SLACK_BOT_TOKEN', 'SLACK_SIGNING_SECRET', 'SLACK_WEBHOOK_URL', 'MCP_AUTH_TOKEN'];
|
|
137
|
+
|
|
138
|
+
const tasks = new Map();
|
|
139
|
+
let _taskSpawner = null;
|
|
140
|
+
|
|
141
|
+
function tasksEnabled() {
|
|
142
|
+
return process.env.LOKI_MCP_TASKS === '1';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function _setTaskSpawnerForTests(fn) {
|
|
146
|
+
_taskSpawner = fn;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function _taskPidForTests(taskId) {
|
|
150
|
+
const t = tasks.get(taskId);
|
|
151
|
+
return t ? t.pid : undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** W3C traceparent -> { traceId, parentSpanId }, or null for anything malformed or all-zero. */
|
|
155
|
+
function parseTraceparent(value) {
|
|
156
|
+
if (typeof value !== 'string') return null;
|
|
157
|
+
const m = TRACEPARENT_RE.exec(value);
|
|
158
|
+
if (!m || /^0+$/.test(m[1]) || /^0+$/.test(m[2])) return null;
|
|
159
|
+
return { traceId: m[1], parentSpanId: m[2] };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function taskChildEnv(trace) {
|
|
163
|
+
const env = {};
|
|
164
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
165
|
+
if (typeof v === 'string' && !TASK_DROPPED_ENV.includes(k)) env[k] = v;
|
|
166
|
+
}
|
|
167
|
+
delete env.LOKI_PARENT_SPAN_ID;
|
|
168
|
+
env.LOKI_NO_BROWSER = '1';
|
|
169
|
+
if (trace) {
|
|
170
|
+
env.LOKI_TRACE_ID = trace.traceId;
|
|
171
|
+
env.LOKI_PARENT_SPAN_ID = trace.parentSpanId;
|
|
172
|
+
}
|
|
173
|
+
return env;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function taskArgs(tool, args) {
|
|
177
|
+
const spec = TASK_TOOLS[Object.prototype.hasOwnProperty.call(TASK_TOOLS, tool) ? tool : ''];
|
|
178
|
+
if (!spec) return { error: 'tool cannot run as a task: ' + String(tool) };
|
|
179
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) return { error: 'arguments must be an object' };
|
|
180
|
+
for (const k of Object.keys(args)) {
|
|
181
|
+
if (!spec.keys.includes(k)) return { error: 'unknown argument: ' + k };
|
|
182
|
+
}
|
|
183
|
+
for (const k of spec.keys) {
|
|
184
|
+
const v = args[k];
|
|
185
|
+
if (v === undefined) continue;
|
|
186
|
+
if (typeof v !== 'string' || v.length > 4096 || v.includes('\0') || v.startsWith('-')) return { error: 'invalid ' + k };
|
|
187
|
+
}
|
|
188
|
+
for (const anyOf of spec.required) {
|
|
189
|
+
if (!anyOf.some((k) => typeof args[k] === 'string' && args[k] !== '')) return { error: anyOf.join(' or ') + ' is required' };
|
|
190
|
+
}
|
|
191
|
+
return { spec, argv: spec.keys.map((k) => (typeof args[k] === 'string' ? args[k] : '')) };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function taskView(t) {
|
|
195
|
+
return { taskId: t.id, status: t.status, tool: t.tool };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function taskCreate(params, id) {
|
|
199
|
+
const p = params || {};
|
|
200
|
+
const a = taskArgs(p.tool, p.arguments);
|
|
201
|
+
if (a.error) return makeError(-32602, a.error, id);
|
|
202
|
+
const trace = parseTraceparent(p._meta && p._meta.traceparent);
|
|
203
|
+
while (tasks.size >= TASK_MAX) {
|
|
204
|
+
const old = Array.from(tasks.values()).find((t) => t.status !== 'working');
|
|
205
|
+
if (!old) return makeError(-32603, 'too many running tasks', id);
|
|
206
|
+
tasks.delete(old.id);
|
|
207
|
+
}
|
|
208
|
+
const adapter = path.resolve(__dirname, '..', '..', 'mcp', 'v10_tools.py');
|
|
209
|
+
const cmd = process.env.LOKI_PYTHON || 'python3';
|
|
210
|
+
const args = ['-I', adapter, a.spec.cmd].concat(a.argv);
|
|
211
|
+
const spawner = _taskSpawner || ((c, av, o) => spawn(c, av, { env: o.env, stdio: ['ignore', 'pipe', 'pipe'] }));
|
|
212
|
+
let child;
|
|
213
|
+
try {
|
|
214
|
+
child = spawner(cmd, args, { env: taskChildEnv(trace) });
|
|
215
|
+
} catch (err) {
|
|
216
|
+
return makeError(-32603, 'could not start task', id);
|
|
217
|
+
}
|
|
218
|
+
const t = { id: crypto.randomUUID(), tool: p.tool, status: 'working', pid: child.pid, stdout: '', text: null, child: child };
|
|
219
|
+
tasks.set(t.id, t);
|
|
220
|
+
if (child.stdout) child.stdout.on('data', (d) => { if (t.stdout.length < 1048576) t.stdout += d; });
|
|
221
|
+
const timer = setTimeout(() => { if (t.status === 'working') { t.status = 'failed'; t.text = 'task timed out'; try { child.kill('SIGTERM'); } catch (e) { /* gone */ } } }, TASK_TIMEOUT_MS);
|
|
222
|
+
timer.unref();
|
|
223
|
+
child.on('error', () => { clearTimeout(timer); if (t.status === 'working') { t.status = 'failed'; t.text = 'adapter could not run'; } });
|
|
224
|
+
child.on('close', (code) => {
|
|
225
|
+
clearTimeout(timer);
|
|
226
|
+
if (t.status !== 'working') return;
|
|
227
|
+
if (code === 0 && t.stdout.trim()) { t.status = 'completed'; t.text = t.stdout.replace(/\n$/, ''); } else { t.status = 'failed'; t.text = 'adapter exited ' + String(code); }
|
|
228
|
+
});
|
|
229
|
+
return makeResult({ task: taskView(t) }, id);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function taskDispatch(method, params, id) {
|
|
233
|
+
if (method === 'tasks/create') return taskCreate(params, id);
|
|
234
|
+
const t = params && typeof params.taskId === 'string' ? tasks.get(params.taskId) : undefined;
|
|
235
|
+
if (!t) return makeError(-32602, 'Unknown taskId', id);
|
|
236
|
+
if (method === 'tasks/get') return makeResult({ task: taskView(t) }, id);
|
|
237
|
+
if (method === 'tasks/result') {
|
|
238
|
+
if (t.status === 'working') return makeError(-32602, 'Task not finished', id);
|
|
239
|
+
if (t.status === 'cancelled') return makeError(-32602, 'Task was cancelled', id);
|
|
240
|
+
return makeResult({ content: [{ type: 'text', text: t.text }], isError: t.status === 'failed' }, id);
|
|
241
|
+
}
|
|
242
|
+
// tasks/cancel: signal only the child this server recorded
|
|
243
|
+
if (t.status === 'working') {
|
|
244
|
+
t.status = 'cancelled';
|
|
245
|
+
try { t.child.kill('SIGTERM'); } catch (e) { /* already gone */ }
|
|
246
|
+
}
|
|
247
|
+
return makeResult({ task: taskView(t) }, id);
|
|
248
|
+
}
|
|
249
|
+
|
|
101
250
|
function getCapabilities() {
|
|
102
251
|
return {
|
|
103
252
|
tools: {},
|
|
@@ -109,7 +258,7 @@ function getCapabilities() {
|
|
|
109
258
|
// JSON-RPC 2.0 request handler
|
|
110
259
|
// ---------------------------------------------------------------------------
|
|
111
260
|
|
|
112
|
-
function handleRequest(request) {
|
|
261
|
+
function handleRequest(request, ctx) {
|
|
113
262
|
// Validate basic JSON-RPC structure
|
|
114
263
|
if (!request || typeof request !== 'object') {
|
|
115
264
|
return makeError(-32600, 'Invalid request', null);
|
|
@@ -129,10 +278,17 @@ function handleRequest(request) {
|
|
|
129
278
|
const isNotification = id === undefined || id === null;
|
|
130
279
|
|
|
131
280
|
// Auth check for tool/resource calls
|
|
132
|
-
if (method === 'tools/call' || method === 'resources/read') {
|
|
281
|
+
if (method === 'tools/call' || method === 'resources/read' || (tasksEnabled() && method.startsWith('tasks/'))) {
|
|
133
282
|
const auth = getAuth();
|
|
283
|
+
const overHttp = !!(ctx && ctx.transport === 'http') && method.startsWith('tasks/');
|
|
284
|
+
// Tasks spawn processes: over HTTP a token is mandatory even when general auth is off (fail closed).
|
|
285
|
+
if (overHttp && !auth.enabled) {
|
|
286
|
+
if (isNotification) return null;
|
|
287
|
+
return makeError(-32001, 'Tasks over HTTP require an auth token (set MCP_AUTH_TOKEN)', id);
|
|
288
|
+
}
|
|
134
289
|
if (auth.enabled) {
|
|
135
|
-
|
|
290
|
+
let validation = auth.validate(request);
|
|
291
|
+
if (overHttp && !validation.valid && ctx.authorization) validation = auth.validateHeader(ctx.authorization);
|
|
136
292
|
if (!validation.valid) {
|
|
137
293
|
if (isNotification) return null;
|
|
138
294
|
return makeError(-32001, validation.error || 'Unauthorized', id);
|
|
@@ -140,17 +296,46 @@ function handleRequest(request) {
|
|
|
140
296
|
}
|
|
141
297
|
}
|
|
142
298
|
|
|
299
|
+
// Stateless profile: a request may carry its protocol version in _meta. No session is created or stored.
|
|
300
|
+
const metaVersion = params && params._meta && typeof params._meta === 'object' ? params._meta[META_PROTOCOL_VERSION] : undefined;
|
|
301
|
+
if (metaVersion !== undefined && !advertisedProtocolVersions().includes(metaVersion)) {
|
|
302
|
+
if (isNotification) return null;
|
|
303
|
+
const err = makeError(-32022, 'Unsupported protocol version: ' + String(metaVersion), id);
|
|
304
|
+
err.error.data = { supported: advertisedProtocolVersions() };
|
|
305
|
+
return err;
|
|
306
|
+
}
|
|
307
|
+
|
|
143
308
|
let result;
|
|
144
309
|
|
|
145
310
|
switch (method) {
|
|
146
311
|
case 'initialize':
|
|
147
312
|
result = {
|
|
313
|
+
protocolVersion: negotiateProtocolVersion(params),
|
|
314
|
+
serverInfo: getServerInfo(),
|
|
315
|
+
capabilities: getCapabilities()
|
|
316
|
+
};
|
|
317
|
+
break;
|
|
318
|
+
|
|
319
|
+
case 'tasks/create':
|
|
320
|
+
case 'tasks/get':
|
|
321
|
+
case 'tasks/result':
|
|
322
|
+
case 'tasks/cancel':
|
|
323
|
+
if (!tasksEnabled()) {
|
|
324
|
+
if (isNotification) return null;
|
|
325
|
+
return makeError(-32601, 'Method not found: ' + method, id);
|
|
326
|
+
}
|
|
327
|
+
return taskDispatch(method, params, id);
|
|
328
|
+
|
|
329
|
+
case 'server/discover':
|
|
330
|
+
result = {
|
|
331
|
+
supportedVersions: advertisedProtocolVersions(),
|
|
148
332
|
serverInfo: getServerInfo(),
|
|
149
333
|
capabilities: getCapabilities()
|
|
150
334
|
};
|
|
151
335
|
break;
|
|
152
336
|
|
|
153
337
|
case 'initialized':
|
|
338
|
+
case 'notifications/initialized':
|
|
154
339
|
// Client acknowledgment -- no response needed
|
|
155
340
|
return isNotification ? null : makeResult({}, id);
|
|
156
341
|
|
|
@@ -359,7 +544,7 @@ function main() {
|
|
|
359
544
|
}
|
|
360
545
|
|
|
361
546
|
// Export for testing
|
|
362
|
-
module.exports = { handleRequest, getTools, getResources, getAuth, getServerInfo, main };
|
|
547
|
+
module.exports = { parseTraceparent, _setTaskSpawnerForTests, _taskPidForTests, SUPPORTED_PROTOCOL_VERSIONS, handleRequest, getTools, getResources, getAuth, getServerInfo, main };
|
|
363
548
|
|
|
364
549
|
// Run if executed directly
|
|
365
550
|
if (require.main === module) {
|
|
@@ -14,6 +14,12 @@ const http = require('http');
|
|
|
14
14
|
// Maximum allowed POST body size (10 MB)
|
|
15
15
|
const MAX_BODY_BYTES = 10 * 1024 * 1024;
|
|
16
16
|
|
|
17
|
+
const LOOPBACK_ORIGIN_RE = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/i;
|
|
18
|
+
|
|
19
|
+
function _isTaskMethod(r) {
|
|
20
|
+
return !!r && typeof r === 'object' && typeof r.method === 'string' && r.method.startsWith('tasks/');
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
class SSETransport {
|
|
18
24
|
constructor(handler, options) {
|
|
19
25
|
this._handler = handler;
|
|
@@ -151,9 +157,24 @@ class SSETransport {
|
|
|
151
157
|
return;
|
|
152
158
|
}
|
|
153
159
|
|
|
160
|
+
// Task methods spawn processes. A text/plain cross-origin POST is a CORS simple request (no preflight), so
|
|
161
|
+
// require a JSON Content-Type and a loopback (or absent) Origin before the handler ever sees them.
|
|
162
|
+
const ctx = { transport: 'http', authorization: req.headers.authorization };
|
|
163
|
+
const touchesTasks = Array.isArray(request) ? request.some(_isTaskMethod) : _isTaskMethod(request);
|
|
164
|
+
if (touchesTasks) {
|
|
165
|
+
const ctype = String(req.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
|
|
166
|
+
const origin = req.headers.origin;
|
|
167
|
+
const reject = (status, message) => {
|
|
168
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
169
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32600, message: message }, id: null }));
|
|
170
|
+
};
|
|
171
|
+
if (ctype !== 'application/json') { reject(415, 'Task requests require Content-Type: application/json'); return; }
|
|
172
|
+
if (origin !== undefined && !LOOPBACK_ORIGIN_RE.test(String(origin))) { reject(403, 'Task requests require a loopback Origin'); return; }
|
|
173
|
+
}
|
|
174
|
+
|
|
154
175
|
// Handle batch
|
|
155
176
|
if (Array.isArray(request)) {
|
|
156
|
-
const promises = request.map((r) => Promise.resolve().then(() => this._handler(r)));
|
|
177
|
+
const promises = request.map((r) => Promise.resolve().then(() => this._handler(r, ctx)));
|
|
157
178
|
Promise.all(promises).then((results) => {
|
|
158
179
|
const responses = results.filter((r) => r !== null);
|
|
159
180
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -171,7 +192,7 @@ class SSETransport {
|
|
|
171
192
|
return;
|
|
172
193
|
}
|
|
173
194
|
|
|
174
|
-
Promise.resolve().then(() => this._handler(request)).then((response) => {
|
|
195
|
+
Promise.resolve().then(() => this._handler(request, ctx)).then((response) => {
|
|
175
196
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
176
197
|
res.end(JSON.stringify(response));
|
|
177
198
|
}).catch(() => {
|
package/tools/audit-docs.py
CHANGED
|
@@ -118,7 +118,7 @@ SKIP_DIRS = {"node_modules", ".git", "dist", "build", "__pycache__",
|
|
|
118
118
|
".venv", "venv", "coverage", ".loki"}
|
|
119
119
|
|
|
120
120
|
# Repo-root files that are not under any SOURCE_DIRS entry but do read env.
|
|
121
|
-
SOURCE_ROOT_FILES = ("docker-compose.yml", "Dockerfile", "Dockerfile.sandbox",
|
|
121
|
+
SOURCE_ROOT_FILES = ("docker/docker-compose.yml", "docker/Dockerfile", "docker/Dockerfile.sandbox",
|
|
122
122
|
"package.json", "server.json")
|
|
123
123
|
|
|
124
124
|
# Markdown trees excluded from the scan, each with the reason a reader can
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as L,r as x,j as e,w as re,Z as ne,m as le,h as ie,p as P,F as ye,U as I,X as ce,L as de,x as oe,M as Ae,T as Z,S as we,l as xe,k as me,y as Ce,E as Fe,a0 as Se,a as U,a2 as he}from"./index-
|
|
1
|
+
import{c as L,r as x,j as e,w as re,Z as ne,m as le,h as ie,p as P,F as ye,U as I,X as ce,L as de,x as oe,M as Ae,T as Z,S as we,l as xe,k as me,y as Ce,E as Fe,a0 as Se,a as U,a2 as he}from"./index-BkbXyH8y.js";import{B as C}from"./Button-rYSyIu7X.js";import{T as $e}from"./trending-up-BhnMcM32.js";import{C as z}from"./clock-BOGqwgt9.js";import{A as Be,U as ue,M as De}from"./Avatar-7GtOQvsO.js";import{A as J}from"./activity-DNQiBDDL.js";import{R as pe}from"./refresh-cw-DsWLXgs_.js";import{G as ze}from"./globe-r-xCP4DE.js";import{C as Le}from"./chevron-left-BJiy55Ht.js";import{C as je}from"./check-CsF-XtQp.js";import{D as Y}from"./dollar-sign-6qWGAkZo.js";import{U as Te}from"./user-Bks3-GYv.js";import{D as Me}from"./database-BJePscl_.js";import{L as Ue}from"./lock-CZZMxP0U.js";import{S as Re}from"./scroll-text-smySzlrX.js";import{S as Ie}from"./shield-alert-BD7FDlh-.js";/**
|
|
2
2
|
* @license lucide-react v0.577.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{j as e}from"./index-
|
|
1
|
+
import{j as e}from"./index-BkbXyH8y.js";import{C as r}from"./clock-BOGqwgt9.js";import{C as a}from"./circle-alert-D6-pQQWq.js";import{C as l}from"./check-CsF-XtQp.js";const i={completed:"bg-[#1FC5A8]/10 text-[#1FC5A8]",running:"bg-[#553DE9]/10 text-[#553DE9]",failed:"bg-[#C45B5B]/10 text-[#C45B5B]",started:"bg-[#D4A03C]/10 text-[#D4A03C]",empty:"bg-[#F8F4F0] text-[#6B6960]",version:"bg-[#553DE9]/10 text-[#553DE9]"};function o({status:t}){switch(t){case"completed":return e.jsx(l,{size:12});case"running":return e.jsxs("span",{className:"relative flex h-2 w-2",children:[e.jsx("span",{className:"animate-ping motion-reduce:animate-none absolute inline-flex h-full w-full rounded-full bg-current opacity-75"}),e.jsx("span",{className:"relative inline-flex rounded-full h-2 w-2 bg-current"})]});case"failed":return e.jsx(a,{size:12});case"started":return e.jsx(r,{size:12});default:return null}}function f({status:t,children:n,className:s=""}){return e.jsxs("span",{className:["inline-flex items-center gap-1 rounded-btn px-2.5 py-0.5 text-xs font-semibold",i[t],s].filter(Boolean).join(" "),children:[e.jsx(o,{status:t}),n]})}export{f as B};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as p,j as t}from"./index-
|
|
1
|
+
import{r as p,j as t}from"./index-BkbXyH8y.js";const m={primary:"bg-[#553DE9] text-white hover:bg-[#4432c4] shadow-button rounded-btn",secondary:"border border-[#553DE9] text-[#553DE9] hover:bg-[#E8E4FD] bg-transparent rounded-btn",ghost:"text-[#36342E] hover:bg-[#F8F4F0] rounded-btn",danger:"bg-[#C45B5B]/10 text-[#C45B5B] border border-[#C45B5B]/20 hover:bg-[#C45B5B]/20 rounded-btn"},b={sm:"px-3 py-1.5 text-xs",md:"px-4 py-2 text-sm",lg:"px-6 py-3 text-base"},u={sm:14,md:16,lg:18};function h({size:e}){return t.jsxs("svg",{className:"animate-spin",width:e,height:e,viewBox:"0 0 24 24",fill:"none",children:[t.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),t.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"})]})}const B=p.forwardRef(({variant:e="primary",size:o="md",icon:n,iconRight:i,loading:r=!1,disabled:a,className:c="",children:l,...x},d)=>{const s=u[o];return t.jsxs("button",{ref:d,disabled:a||r,className:["inline-flex items-center justify-center gap-2 font-medium transition-colors",m[e],b[o],(a||r)&&"opacity-60 cursor-not-allowed",c].filter(Boolean).join(" "),...x,children:[r?t.jsx(h,{size:s}):n?t.jsx(n,{size:s}):null,l,i&&!r&&t.jsx(i,{size:s})]})});B.displayName="Button";export{B};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as $,r as l,j as e,G as we,Q as Se,F as Ce,h as Re,T as U,Z as Ee,a as S,V as Me,S as $e,Y as Te,X as Pe,P as ie,i as Ae,g as ze,C as Ie,l as Le,N as I,J as _e,K as De,O as Fe,u as Oe}from"./index-
|
|
1
|
+
import{c as $,r as l,j as e,G as we,Q as Se,F as Ce,h as Re,T as U,Z as Ee,a as S,V as Me,S as $e,Y as Te,X as Pe,P as ie,i as Ae,g as ze,C as Ie,l as Le,N as I,J as _e,K as De,O as Fe,u as Oe}from"./index-BkbXyH8y.js";import{P as K,S as He,E as Be}from"./ErrorBoundary-CwtvY4xs.js";import{C as qe,F as Ge,M as Ue,H as Ke,a as We,G as Ve,T as Qe}from"./thumbs-up-CAsFzbyE.js";import{C as oe}from"./check-CsF-XtQp.js";import{C as Ye}from"./clock-BOGqwgt9.js";import{D as Xe}from"./dollar-sign-6qWGAkZo.js";import{U as Ze}from"./upload-JVHfdwoA.js";import{L as V}from"./loader-circle-3gosTei9.js";/**
|
|
2
2
|
* @license lucide-react v0.577.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|