openmerit 0.3.4 → 0.3.6
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/CHANGELOG.md +9 -0
- package/dist/pi/scout-extension.mjs +30 -0
- package/dist/terminal/scout-update.json +1 -1
- package/dist/terminal/src/browser.d.mts +9 -0
- package/dist/terminal/src/browser.mjs +1 -1
- package/dist/terminal/src/cli.mjs +3 -0
- package/dist/terminal/src/connections.js +151 -0
- package/dist/terminal/src/install.d.mts +2 -0
- package/dist/terminal/src/install.mjs +5 -2
- package/dist/terminal/src/scout-instructions.js +30 -0
- package/dist/terminal/src/scout.js +18 -1
- package/dist/terminal/src/telemetry.d.mts +30 -0
- package/dist/terminal/src/updates.js +1 -1
- package/docs/index.md +3 -0
- package/docs/scout-close-loop.md +295 -0
- package/docs/scout-data-policy.md +47 -0
- package/docs/scout.md +1 -0
- package/docs/security.md +26 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.6 — 2026-11-06
|
|
4
|
+
|
|
5
|
+
- Distinguish a connected Langfuse key from incomplete score-import setup. Connection status exposes only score-mapping and model-alias counts, and the web UI explains when either is missing. The CLI preserves those counts when the server provides them. Requires the corresponding Scout deployment and npm patch; does not change credentials, mappings or experiment attribution.
|
|
6
|
+
- Update an existing Langfuse connection's non-secret settings through CLI, MCP or API without retrieving or re-entering its Viewer key. Omit both Viewer credentials; Scout reuses and reverifies the encrypted key internally. New connections still require a protected Viewer pair, and partial pairs are rejected. Settings changes invalidate imported aggregates; sync again afterwards. Requires the matching Scout deployment.
|
|
7
|
+
|
|
8
|
+
## 0.3.5 — 2026-11-05
|
|
9
|
+
|
|
10
|
+
- Add optional headless Langfuse setup through the CLI, HTTP API and `scout_connection` MCP tool. A separately approved `scout:connections` key manages the authenticated user's connection and evaluation data; recommendation keys retain their existing permission. Viewer credentials enter through protected CLI input or trusted client configuration, never model-visible tool arguments. The web connection UI remains available, with revised spacing, mobile controls and the official Langfuse logo. Hosted features require a matching Scout deployment; saving Langfuse credentials also requires its dedicated encryption secret. Publishing this package does not deploy or enable the hosted connector.
|
|
11
|
+
|
|
3
12
|
## 0.3.4 — 2026-11-05
|
|
4
13
|
|
|
5
14
|
- `openmerit scout --results` refuses a file with more than 50 results before sending it, instead of failing with HTTP 413; split larger reports into files of up to 50.
|
|
@@ -141,6 +141,36 @@ aggregates only, never inputs, outputs or records.
|
|
|
141
141
|
Scout ranks them above public benchmarks for this user only; models whose
|
|
142
142
|
\`quality.measured\` is true rest on them.
|
|
143
143
|
|
|
144
|
+
Swap one agent at a time, holding the other agents, prompts, fixtures and controls
|
|
145
|
+
fixed. Use each agent's own checks to attribute failures. If all candidates score
|
|
146
|
+
100%, the eval cannot separate their quality: add harder labelled cases before
|
|
147
|
+
claiming equivalent quality. Cost may break the observed tie, not prove equality.
|
|
148
|
+
|
|
149
|
+
If the user has Langfuse, use \`openmerit connect langfuse --help\`: the web UI is
|
|
150
|
+
optional. Headless setup requires a separate connection-manager approval with
|
|
151
|
+
\`openmerit connect langfuse --authorize --no-open\`. Relay its approval link and
|
|
152
|
+
expiry; after approval, rerun that command. Existing recommendation keys cannot
|
|
153
|
+
manage connections. The user creates a separate Viewer key; receive it through
|
|
154
|
+
protected stdin or a secret-manager-injected environment, never chat, tool
|
|
155
|
+
arguments, shell literals, traced commands or tracked files. The CLI accepts
|
|
156
|
+
non-secret settings via \`--config mappings.json\` and reads LANGFUSE_PUBLIC_KEY
|
|
157
|
+
and LANGFUSE_SECRET_KEY from its environment, or reads full settings via --stdin.
|
|
158
|
+
A separate trusted MCP connection with the management key exposes
|
|
159
|
+
\`scout_connection\` (status/configure/sync/disconnect/clear). For configure, keys
|
|
160
|
+
must come from client-controlled HTTP headers outside model context; if the
|
|
161
|
+
client cannot do this securely, use protected CLI input or the web UI instead.
|
|
162
|
+
After they confirm connection and score/model mappings, call \`scout_models\` with
|
|
163
|
+
\`sync: "langfuse"\` to refresh aggregate measurements before recommending. The CLI
|
|
164
|
+
equivalent is \`openmerit scout --conversation <id> --sync langfuse\`. Never read
|
|
165
|
+
trace bodies to work around missing mappings or untagged runs; use the aggregate
|
|
166
|
+
report path instead. Results are stored per user, used in new conversations and
|
|
167
|
+
expire after 90 days. Disconnect deletes the Viewer key and imported aggregates;
|
|
168
|
+
Delete all evaluation data also deletes manually reported aggregates. Obtain
|
|
169
|
+
consent before deleting: headless CLI requires --yes, MCP requires confirm: true.
|
|
170
|
+
Deleting the connection does not revoke the separate management device key.
|
|
171
|
+
Past conversation text is not erased. Never put record IDs, emails or secrets in
|
|
172
|
+
\`scope\` or \`source\`; use labels such as "coding agent" and "held-out eval".
|
|
173
|
+
|
|
144
174
|
## Boundaries
|
|
145
175
|
|
|
146
176
|
Ask before adding providers or credentials, spending, deploying or external
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"format":1,"contract":"scout-recommend-
|
|
1
|
+
{"format":1,"contract":"scout-recommend-v2","version":"0.3.6","skill":"---\nname: openmerit-scout\ndescription: Consults OpenMerit Scout before choosing, adding, downloading, replacing or configuring any application inference model, including implicit choices while building features, pretrained browser/local models, SDK defaults, embeddings, OCR, speech, classification, detection, segmentation, LLMs and agents. Not for unrelated edits, ordinary inference calls or this coding agent's own model.\n---\n\n# OpenMerit · Scout\n\nBefore introducing, recommending, replacing or configuring an application\ninference model, call the `scout_models` tool on the `openmerit` MCP server.\nThis applies even when the user never mentions models or Scout. Downloading local\nweights, using a pretrained browser model or accepting an SDK default is a model\ndecision: consult Scout before model-specific code, installs or downloads.\nPreserve the user's explicit model choice; assess it rather than silently replacing\nit. Leave unchanged, approved application selections in place.\n\n## Project policy\n\nRead `.openmerit/scout.json` before searching. If it is absent, ask once whether this\nproject uses **existing-only** (only endpoints it can already reach) or\n**existing-first** (prefer them, ask before searching the whole catalogue), and\nwhich public catalogue endpoint IDs it can access. Save the answer:\n\n```sh\nnpx -y openmerit@latest project --policy existing-only --endpoint-id <id> ...\n```\n\nNever infer access from a model name, SDK or environment-variable name. Never\nrecord credentials, private URLs, prompts or customer data. An empty inventory\nis not permission to search all models.\n\n## Searching\n\nCall `scout_models` with a non-confidential task description as `prompt` and\n`endpointIds` set to the saved inventory, including `[]` when empty. Send\nonly task requirements, never source files, secrets or real customer records.\nIf the MCP tool is unavailable, run\n`npx -y openmerit@latest scout \"<task>\" --json`; it applies the saved inventory.\n\nScout is a conversation with the agent behind openmerit.ai/scout. When\n`status` is `needs_answers`, fill in what the user or project already states,\nshow the user Scout's brief and remaining questions, and wait for their answers.\nThen call again with the returned `conversationId` and `brief` set to the\nrewritten components, or `skip: true` if the user wants Scout to assume. Never\nanswer Scout's questions with guesses. Send later refinements to the same\n`conversationId`. A call with only a `conversationId` reads an interrupted turn.\n\nWhen `status` is `recommended`, Scout has split the work into `tasks`, one\nper model, each with up to five ranked `models`; cover every task. Each\nmodel's `cost` names the `endpointId` and `providerModelId` to call.\nExisting-only never broadens. For existing-first, if no existing candidate fits a\ntask, explain the gap and ask before a new conversation without `endpointIds`.\nCompare quality evidence, reference cost/time and watch-outs. Prefer the cheapest\nusable option the evidence supports, without claiming benchmarks prove\napplication accuracy. Use the project's actual SDK and protocol.\n\n## Measuring candidates\n\nEach task also has an `evaluation`: the checks (per model call, agent run or\nend-to-end outcome), labelled examples per candidate and when to stop a losing\ncandidate. Offer to run it before the user commits to a model. Use the evals the\nproject already has, such as datasets and experiments in its eval or tracing\nplatform, or its own eval scripts and fixtures: add the shortlisted candidates as\nextra runs of those evals. Do not build a second eval or copy their datasets.\nIf the project has no eval for the task, propose a minimal one built from its\nexisting traces and the decisions people already make, and create it only where\nand how the user agrees. Ask before spending on model calls or writing to an\neval platform.\n\nReport what you measured with `scout_models`: the same `conversationId` and\n`results`, one aggregate per model and metric (`level`, `task` title, `scope`,\n`model` endpoint or model ID, `metric`, `value`, `higherIsBetter`,\n`samples`, `source`). Set `kind` to `usd` or `seconds` for measured cost\nor time (per call at the task level, per run at the agent level), so Scout uses\nyour cost instead of its estimate. Report each agent's own checks at the agent\nlevel: an end-to-end result also measures the agents you did not swap. Send\naggregates only, never inputs, outputs or records.\nScout ranks them above public benchmarks for this user only; models whose\n`quality.measured` is true rest on them.\n\nSwap one agent at a time, holding the other agents, prompts, fixtures and controls\nfixed. Use each agent's own checks to attribute failures. If all candidates score\n100%, the eval cannot separate their quality: add harder labelled cases before\nclaiming equivalent quality. Cost may break the observed tie, not prove equality.\n\nIf the user has Langfuse, use `openmerit connect langfuse --help`: the web UI is\noptional. Headless setup requires a separate connection-manager approval with\n`openmerit connect langfuse --authorize --no-open`. Relay its approval link and\nexpiry; after approval, rerun that command. Existing recommendation keys cannot\nmanage connections. The user creates a separate Viewer key; receive it through\nprotected stdin or a secret-manager-injected environment, never chat, tool\narguments, shell literals, traced commands or tracked files. The CLI accepts\nnon-secret settings via `--config mappings.json` and reads LANGFUSE_PUBLIC_KEY\nand LANGFUSE_SECRET_KEY from its environment, or reads full settings via --stdin.\nA separate trusted MCP connection with the management key exposes\n`scout_connection` (status/configure/sync/disconnect/clear). For configure, keys\nmust come from client-controlled HTTP headers outside model context; if the\nclient cannot do this securely, use protected CLI input or the web UI instead.\nAfter they confirm connection and score/model mappings, call `scout_models` with\n`sync: \"langfuse\"` to refresh aggregate measurements before recommending. The CLI\nequivalent is `openmerit scout --conversation <id> --sync langfuse`. Never read\ntrace bodies to work around missing mappings or untagged runs; use the aggregate\nreport path instead. Results are stored per user, used in new conversations and\nexpire after 90 days. Disconnect deletes the Viewer key and imported aggregates;\nDelete all evaluation data also deletes manually reported aggregates. Obtain\nconsent before deleting: headless CLI requires --yes, MCP requires confirm: true.\nDeleting the connection does not revoke the separate management device key.\nPast conversation text is not erased. Never put record IDs, emails or secrets in\n`scope` or `source`; use labels such as \"coding agent\" and \"held-out eval\".\n\n## Boundaries\n\nAsk before adding providers or credentials, spending, deploying or external\nwrites. Report recommendation, endpoint connectivity and application checks as\nseparate results, each only after it succeeds. Do not switch this coding agent's\nown model.\n\nIf Scout fails or has no suitable candidate/evidence for the required deployment,\nsay so and pause the model decision; do not substitute a remembered recommendation\nor SDK default. Continue only model-independent work. Local/browser models are not\nexempt when the catalogue lacks coverage. When an install or login waits for approval (exit code\n10), show the user the approval link and its UTC expiry in your reply, not only in\ncommand output, then rerun the same command once they approve. If it reports that the device key is missing,\nexpired or revoked, run `npx -y openmerit@latest login` and then\n`npx -y openmerit@latest install` again. Never ask the user to paste a key.\n","rule":"# Application model decisions · OpenMerit Scout\n\nBefore choosing, recommending, adding, downloading, replacing or configuring\nANY application inference model, load the `openmerit-scout` skill and consult\n`scout_models` on the `openmerit` MCP server before committing to a model.\nThis applies even when the user only asks you to build a feature, never mentions\nmodels or Scout, or names a model themselves (assess it; do not silently replace it).\n\nA pretrained model, local weights, browser inference, an SDK's default model,\nembeddings, OCR, speech, image classification, detection and segmentation all\ncount as model choices, not implementation details. Identify this decision before\nwriting model-specific code, installing a model package or downloading weights.\n\nFollow the skill's project access policy. If it is missing, ask and save it first.\nScout may answer with clarifying questions: relay them to the user and continue\nthe same Scout conversation with their answers before choosing.\nIf Scout is unavailable, fails or has no suitable evidence/candidate, disclose the\ngap and pause the model decision; do not fall back to a remembered or SDK-default\nmodel. You may continue work that does not commit to a model.\n\nDo not consult Scout for unrelated edits, normal inference calls using an unchanged\napproved model, or this coding agent's own model. Do not reselect approved models\nunless the task introduces or changes a model decision.\n\nWhen the user says \"update openmerit\", run\n`npx -y openmerit@latest update --scope user` (use `--scope project` if this\nrule is in the project's .claude/rules directory). Report the result, including\nany conflict or manual-upgrade requirement. Do not delete edited files, bypass\npermissions or rerun login automatically. A new session loads updated instructions.\n"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function browserCommand(url: any, platform: NodeJS.Platform | undefined, env: NodeJS.ProcessEnv | undefined, allowed: any): (string | any[])[] | null;
|
|
2
|
+
export function openBrowser(url: any, { platform, env, spawnProcess, timeoutMs, allowed }?: {
|
|
3
|
+
platform?: NodeJS.Platform | undefined;
|
|
4
|
+
env?: NodeJS.ProcessEnv | undefined;
|
|
5
|
+
spawnProcess?: typeof spawn | undefined;
|
|
6
|
+
timeoutMs?: number | undefined;
|
|
7
|
+
allowed?: ((url: string) => boolean) | undefined;
|
|
8
|
+
}): Promise<any>;
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
@@ -10,7 +10,7 @@ export function browserCommand(url, platform = process.platform, env = process.e
|
|
|
10
10
|
return null;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export async function openBrowser(url, { platform = process.platform, env = process.env, spawnProcess = spawn, timeoutMs = 3000, allowed } = {}) {
|
|
13
|
+
export async function openBrowser(url, { platform = process.platform, env = process.env, spawnProcess = spawn, timeoutMs = 3000, allowed = /** @type {(url: string) => boolean} */ (() => false) } = {}) {
|
|
14
14
|
const command = browserCommand(url, platform, env, allowed);
|
|
15
15
|
if (!command) return false;
|
|
16
16
|
return new Promise(resolve => {
|
|
@@ -6,11 +6,14 @@ const help = `OpenMerit
|
|
|
6
6
|
openmerit scout --help Ask Scout which models fit your tasks
|
|
7
7
|
openmerit install --help Connect OpenMerit to your coding agent
|
|
8
8
|
openmerit update --help Update installed Scout instructions
|
|
9
|
+
openmerit connect langfuse --help Connect through CLI or the optional web UI
|
|
10
|
+
openmerit disconnect langfuse Delete the connection and imports
|
|
9
11
|
openmerit --help Show this help
|
|
10
12
|
`;
|
|
11
13
|
|
|
12
14
|
async function main() {
|
|
13
15
|
const args = process.argv.slice(2);
|
|
16
|
+
if (['connect', 'disconnect'].includes(args[0])) { const { connection } = await import('./connections.js'); process.exitCode = await connection(args); return; }
|
|
14
17
|
if (['install', 'status', 'remove', 'login', 'logout', 'project', 'doctor', 'update'].includes(args[0])) { const { main } = await import('./install.mjs'); process.exitCode = await main(args); return; }
|
|
15
18
|
if (args[0] === 'scout') { const { main } = await import('./scout.js'); process.exitCode = await main(args.slice(1)); return; }
|
|
16
19
|
if (!args.length || args.includes('--help') || args.includes('-h')) { console.log(help); return; }
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { credentialsPath, readCredentials } from "./scout-credentials.js";
|
|
4
|
+
import { openBrowser } from './browser.mjs';
|
|
5
|
+
import { main as install } from './install.mjs';
|
|
6
|
+
const help = `OpenMerit · Langfuse connections
|
|
7
|
+
|
|
8
|
+
openmerit connect langfuse Open the optional web UI
|
|
9
|
+
openmerit connect langfuse --authorize [--no-open] [--json]
|
|
10
|
+
openmerit connect langfuse --config mappings.json [--json]
|
|
11
|
+
openmerit connect langfuse --stdin [--json]
|
|
12
|
+
openmerit connect langfuse --status|--sync [--json]
|
|
13
|
+
openmerit disconnect langfuse --yes [--json]
|
|
14
|
+
openmerit disconnect langfuse --delete-all --yes [--json]
|
|
15
|
+
|
|
16
|
+
Authorize a separate connection-manager key once. First-time approval still
|
|
17
|
+
requires account sign-in; --no-open prints a link for approval on any device.
|
|
18
|
+
Rerun --authorize if pending (exit 10). This never replaces recommendation keys.
|
|
19
|
+
The management key is saved separately with owner-only file permissions.
|
|
20
|
+
|
|
21
|
+
--config reads non-secret region, from (YYYY-MM-DD), tagPrefix, mappings and models.
|
|
22
|
+
For a new connection, supply LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY through
|
|
23
|
+
a secret manager. Omit both to update settings using the saved Viewer key.
|
|
24
|
+
--stdin reads the full JSON settings including the Viewer key pair from a protected
|
|
25
|
+
pipe (maximum 18 KB), never echoes them, and does not save them locally.
|
|
26
|
+
Never put keys in arguments, chat, shell history or a tracked file. Use a separate
|
|
27
|
+
Viewer key, not tracing keys. OPENMERIT_CONNECTION_KEY can supply an already
|
|
28
|
+
approved management key to unattended jobs; otherwise the saved key is used.
|
|
29
|
+
|
|
30
|
+
Disconnect deletes the key pair and imports, not reported results or conversations.
|
|
31
|
+
--delete-all also removes reported results. Both require --yes for headless use.
|
|
32
|
+
Without --yes, disconnect opens the optional browser confirmation.
|
|
33
|
+
`;
|
|
34
|
+
const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
35
|
+
async function settings(file, stdin, root) {
|
|
36
|
+
let text = '';
|
|
37
|
+
if (stdin) {
|
|
38
|
+
if (process.stdin.isTTY)
|
|
39
|
+
throw new Error('--stdin requires a protected JSON pipe, not interactive key entry.');
|
|
40
|
+
process.stdin.setEncoding('utf8');
|
|
41
|
+
let bytes = 0;
|
|
42
|
+
for await (const chunk of process.stdin) {
|
|
43
|
+
bytes += Buffer.byteLength(chunk);
|
|
44
|
+
if (bytes > 18_000)
|
|
45
|
+
throw new Error('Connection settings exceed 18 KB.');
|
|
46
|
+
text += String(chunk);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
try {
|
|
51
|
+
text = await readFile(resolve(root, file ?? ''), 'utf8');
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new Error('Could not read connection settings.');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (Buffer.byteLength(text) > 18_000)
|
|
58
|
+
throw new Error('Connection settings exceed 18 KB.');
|
|
59
|
+
let config;
|
|
60
|
+
try {
|
|
61
|
+
config = JSON.parse(text);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new Error('Connection settings must be a JSON object.');
|
|
65
|
+
}
|
|
66
|
+
const fields = ['region', 'from', 'tagPrefix', 'mappings', 'models', ...(stdin ? ['publicKey', 'secretKey'] : [])];
|
|
67
|
+
if (!object(config) || Object.keys(config).some(key => !fields.includes(key)))
|
|
68
|
+
throw new Error('Use non-secret settings in --config; credentials must come from a secret manager or --stdin.');
|
|
69
|
+
if (!stdin)
|
|
70
|
+
config = { ...config, publicKey: process.env.LANGFUSE_PUBLIC_KEY, secretKey: process.env.LANGFUSE_SECRET_KEY };
|
|
71
|
+
if (object(config) && !stdin && config.publicKey === undefined && config.secretKey === undefined) {
|
|
72
|
+
delete config.publicKey;
|
|
73
|
+
delete config.secretKey;
|
|
74
|
+
return config;
|
|
75
|
+
}
|
|
76
|
+
if (!object(config) || typeof config.publicKey !== 'string' || typeof config.secretKey !== 'string' || !config.publicKey || !config.secretKey)
|
|
77
|
+
throw new Error('Supply a separate Viewer key pair through a secret manager or --stdin.');
|
|
78
|
+
return config;
|
|
79
|
+
}
|
|
80
|
+
export async function connection(args, { root = process.cwd(), print = console.log, printError = console.error } = {}) {
|
|
81
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
82
|
+
print(help);
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
const [command, provider, ...flags] = args;
|
|
86
|
+
if (!['connect', 'disconnect'].includes(command ?? '') || provider !== 'langfuse')
|
|
87
|
+
throw new Error('Use openmerit connect langfuse --help.');
|
|
88
|
+
let file;
|
|
89
|
+
const options = new Set();
|
|
90
|
+
for (let i = 0; i < flags.length; i++) {
|
|
91
|
+
const flag = flags[i] ?? '';
|
|
92
|
+
if (options.has(flag))
|
|
93
|
+
throw new Error('Duplicate connection option.');
|
|
94
|
+
options.add(flag);
|
|
95
|
+
if (flag === '--config') {
|
|
96
|
+
file = flags[++i];
|
|
97
|
+
if (!file || file.startsWith('-'))
|
|
98
|
+
throw new Error('--config needs a settings file.');
|
|
99
|
+
}
|
|
100
|
+
else if (!['--stdin', '--authorize', '--no-open', '--status', '--sync', '--yes', '--delete-all', '--json'].includes(flag))
|
|
101
|
+
throw new Error('Unknown connection option. Use --help; never pass keys as arguments.');
|
|
102
|
+
}
|
|
103
|
+
const operation = ['--config', '--stdin', '--authorize', '--status', '--sync'].filter(flag => options.has(flag));
|
|
104
|
+
if (operation.length > 1 || command === 'disconnect' && operation.length || command === 'connect' && (options.has('--yes') || options.has('--delete-all'))
|
|
105
|
+
|| options.has('--no-open') && !options.has('--authorize'))
|
|
106
|
+
throw new Error('Choose one connection action. Use --help.');
|
|
107
|
+
if (options.has('--delete-all') && !options.has('--yes'))
|
|
108
|
+
throw new Error('--delete-all requires --yes.');
|
|
109
|
+
if (options.has('--authorize'))
|
|
110
|
+
return install(['login', '--client', 'connector', '--scope', 'user', ...(options.has('--no-open') ? ['--no-open'] : []), ...(options.has('--json') ? ['--json'] : [])], { root, print, printError });
|
|
111
|
+
if (!operation.length && !options.has('--yes')) {
|
|
112
|
+
const url = 'https://openmerit.ai/connect/?integration=langfuse';
|
|
113
|
+
print(`Manage Langfuse in your signed-in browser: ${url}`);
|
|
114
|
+
await openBrowser(url, { allowed: (value) => value === url }).catch(() => false);
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
const server = (process.env.OPENMERIT_URL ?? 'https://openmerit.ai').replace(/\/+$/, '');
|
|
118
|
+
const url = new URL(server);
|
|
119
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== '/' || (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))))
|
|
120
|
+
throw new Error('Use an HTTPS OpenMerit origin, or localhost for development.');
|
|
121
|
+
const credentials = readCredentials(credentialsPath('connector', 'user', root));
|
|
122
|
+
const key = process.env.OPENMERIT_CONNECTION_KEY ?? (credentials?.server === server ? credentials.key : undefined);
|
|
123
|
+
if (!key || !/^om_[A-Za-z0-9_-]{43}$/.test(key))
|
|
124
|
+
throw new Error('Authorize connection management first: openmerit connect langfuse --authorize --no-open');
|
|
125
|
+
// Read secrets only after authorization. No credential values reach stdout,
|
|
126
|
+
// local files, tool arguments, analytics or errors, even on malformed input.
|
|
127
|
+
const config = file || options.has('--stdin') ? await settings(file, options.has('--stdin'), root) : undefined;
|
|
128
|
+
const path = options.has('--delete-all') ? 'evaluations' : options.has('--sync') ? 'langfuse/sync' : 'langfuse';
|
|
129
|
+
const method = command === 'disconnect' ? 'DELETE' : config || options.has('--sync') ? 'POST' : 'GET';
|
|
130
|
+
let response;
|
|
131
|
+
try {
|
|
132
|
+
response = await fetch(`${server}/connect/api/${path}`, { method, redirect: 'error', signal: AbortSignal.timeout(120_000),
|
|
133
|
+
headers: { Authorization: `Bearer ${key}`, ...(config ? { 'Content-Type': 'application/json' } : {}), 'X-Scout-Analytics': 'off' },
|
|
134
|
+
...(config ? { body: JSON.stringify(config) } : {}),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
throw new Error('Could not reach OpenMerit. Check connection status before retrying a change.');
|
|
139
|
+
}
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
await response.body?.cancel();
|
|
142
|
+
throw new Error(response.status === 401 || response.status === 403 ? 'Management key rejected. Run openmerit connect langfuse --authorize --no-open.'
|
|
143
|
+
: `Connection request failed (HTTP ${response.status}). Check Viewer permissions, region and mappings; check status before retrying.`);
|
|
144
|
+
}
|
|
145
|
+
const value = await response.json();
|
|
146
|
+
if (!object(value))
|
|
147
|
+
throw new Error('OpenMerit returned an invalid connection response.');
|
|
148
|
+
const state = Object.fromEntries(Object.entries(value).filter(([name]) => ['connected', 'connectedAt', 'syncedAt', 'mappingCount', 'modelAliasCount', 'experiments', 'unmapped', 'results', 'busy', 'cached', 'deleted'].includes(name)));
|
|
149
|
+
print(JSON.stringify(state, null, 2));
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
@@ -19,7 +19,7 @@ const exitCodes = { ok: 0, error: 1, pending: 10, denied: 11, conflict: 20, unre
|
|
|
19
19
|
const exec = promisify(execFile);
|
|
20
20
|
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
21
21
|
const policyPath = '.openmerit/scout.json';
|
|
22
|
-
const clients = { 'claude-code': 'Claude Code', pi: 'Pi' };
|
|
22
|
+
const clients = { 'claude-code': 'Claude Code', pi: 'Pi', connector: 'OpenMerit connection manager' };
|
|
23
23
|
|
|
24
24
|
const help = `OpenMerit · coding-agent setup
|
|
25
25
|
|
|
@@ -45,6 +45,8 @@ It opens the approval page and waits up to 9 minutes for the Approve click
|
|
|
45
45
|
pending, rerun the same command: it resumes the same request.
|
|
46
46
|
--client defaults to the coding agent running the command. --scope defaults to user.
|
|
47
47
|
--plan shows the changes without making them. Keys are never printed.
|
|
48
|
+
login/logout also accept --client connector for a separate connection-management
|
|
49
|
+
key. This grants no model recommendations and changes no coding-agent install.
|
|
48
50
|
|
|
49
51
|
Exit codes: 0 done, 10 approval pending, 11 denied or expired, 20 conflict,
|
|
50
52
|
30 OpenMerit unreachable, 1 other error.
|
|
@@ -471,6 +473,7 @@ async function runMain(args, { root = process.cwd(), print = console.log, printE
|
|
|
471
473
|
options.client ??= detectClient() ?? (['install', 'status', 'remove'].includes(action) ? undefined : 'claude-code');
|
|
472
474
|
if (!options.client) throw new Error('Choose the coding agent with --client claude-code or --client pi.');
|
|
473
475
|
if (!clients[options.client]) throw new Error(`Unsupported client "${options.client}". Supported: ${Object.keys(clients).join(', ')}.`);
|
|
476
|
+
if (options.client === 'connector' && !['login', 'logout'].includes(action)) throw new Error('The connector client supports login/logout only. Use openmerit connect langfuse --help.');
|
|
474
477
|
}
|
|
475
478
|
root = resolve(root);
|
|
476
479
|
telemetry.root = root;
|
|
@@ -510,7 +513,7 @@ async function runMain(args, { root = process.cwd(), print = console.log, printE
|
|
|
510
513
|
: action === 'project' ? await project(root, options)
|
|
511
514
|
: action === 'doctor' ? await doctor(root, options)
|
|
512
515
|
: action === 'logout' ? { action, revoked: await revokeKey(telemetry), note: 'Signed out. This device key was revoked and deleted. Agents configured with it stop working until you run install again.' }
|
|
513
|
-
: (await revokeKey(telemetry), await login({ client: options.client, scope: options.scope, open: options.open, log, wait, telemetry }), { action, signedIn: true, note: `Signed in. Run "openmerit install --client ${options.client} --scope ${options.scope}" to refresh this installation.` });
|
|
516
|
+
: (await revokeKey(telemetry), await login({ client: options.client, scope: options.scope, open: options.open, log, wait, telemetry }), { action, signedIn: true, note: options.client === 'connector' ? 'Connection management authorized. Recommendation keys and coding-agent installations are unchanged.' : `Signed in. Run "openmerit install --client ${options.client} --scope ${options.scope}" to refresh this installation.` });
|
|
514
517
|
if (result.conflicts?.length && telemetry.outcome !== 'error') { telemetry.outcome = 'conflict'; telemetry.properties.code = 'install_conflict'; }
|
|
515
518
|
if (options.plan) await telemetry.step(telemetry.stage, telemetry.outcome, { applied: false });
|
|
516
519
|
print(options.json ? JSON.stringify({ ok: action !== 'update' || !['conflict', 'retry', 'manual_required'].includes(result.status), ...result }, null, 2) : render(result));
|
|
@@ -107,6 +107,36 @@ aggregates only, never inputs, outputs or records.
|
|
|
107
107
|
Scout ranks them above public benchmarks for this user only; models whose
|
|
108
108
|
\`quality.measured\` is true rest on them.
|
|
109
109
|
|
|
110
|
+
Swap one agent at a time, holding the other agents, prompts, fixtures and controls
|
|
111
|
+
fixed. Use each agent's own checks to attribute failures. If all candidates score
|
|
112
|
+
100%, the eval cannot separate their quality: add harder labelled cases before
|
|
113
|
+
claiming equivalent quality. Cost may break the observed tie, not prove equality.
|
|
114
|
+
|
|
115
|
+
If the user has Langfuse, use \`openmerit connect langfuse --help\`: the web UI is
|
|
116
|
+
optional. Headless setup requires a separate connection-manager approval with
|
|
117
|
+
\`openmerit connect langfuse --authorize --no-open\`. Relay its approval link and
|
|
118
|
+
expiry; after approval, rerun that command. Existing recommendation keys cannot
|
|
119
|
+
manage connections. The user creates a separate Viewer key; receive it through
|
|
120
|
+
protected stdin or a secret-manager-injected environment, never chat, tool
|
|
121
|
+
arguments, shell literals, traced commands or tracked files. The CLI accepts
|
|
122
|
+
non-secret settings via \`--config mappings.json\` and reads LANGFUSE_PUBLIC_KEY
|
|
123
|
+
and LANGFUSE_SECRET_KEY from its environment, or reads full settings via --stdin.
|
|
124
|
+
A separate trusted MCP connection with the management key exposes
|
|
125
|
+
\`scout_connection\` (status/configure/sync/disconnect/clear). For configure, keys
|
|
126
|
+
must come from client-controlled HTTP headers outside model context; if the
|
|
127
|
+
client cannot do this securely, use protected CLI input or the web UI instead.
|
|
128
|
+
After they confirm connection and score/model mappings, call \`scout_models\` with
|
|
129
|
+
\`sync: "langfuse"\` to refresh aggregate measurements before recommending. The CLI
|
|
130
|
+
equivalent is \`openmerit scout --conversation <id> --sync langfuse\`. Never read
|
|
131
|
+
trace bodies to work around missing mappings or untagged runs; use the aggregate
|
|
132
|
+
report path instead. Results are stored per user, used in new conversations and
|
|
133
|
+
expire after 90 days. Disconnect deletes the Viewer key and imported aggregates;
|
|
134
|
+
Delete all evaluation data also deletes manually reported aggregates. Obtain
|
|
135
|
+
consent before deleting: headless CLI requires --yes, MCP requires confirm: true.
|
|
136
|
+
Deleting the connection does not revoke the separate management device key.
|
|
137
|
+
Past conversation text is not erased. Never put record IDs, emails or secrets in
|
|
138
|
+
\`scope\` or \`source\`; use labels such as "coding agent" and "held-out eval".
|
|
139
|
+
|
|
110
140
|
## Boundaries
|
|
111
141
|
|
|
112
142
|
Ask before adding providers or credentials, spending, deploying or external
|
|
@@ -12,6 +12,9 @@ const help = `OpenMerit · Scout
|
|
|
12
12
|
openmerit scout "<the work models need to do>" [--all-endpoints] [--json]
|
|
13
13
|
openmerit scout --conversation <id> [--brief <component>=<text> ...] [--skip] ["<follow-up>"] [--json]
|
|
14
14
|
openmerit scout --conversation <id> --results <file.json> [--json]
|
|
15
|
+
openmerit scout --conversation <id> --sync langfuse [--json]
|
|
16
|
+
openmerit connect langfuse
|
|
17
|
+
openmerit disconnect langfuse
|
|
15
18
|
|
|
16
19
|
Asks Scout, the agent behind openmerit.ai/scout. Scout may first return a task
|
|
17
20
|
brief with clarifying questions: answer them with --brief (components: input,
|
|
@@ -24,6 +27,13 @@ samples, source}), never inputs or outputs. kind is quality (the default), usd
|
|
|
24
27
|
or seconds; measured usd replaces Scout's cost estimate. Scout ranks them above
|
|
25
28
|
public benchmarks, for your recommendations only. A turn usually takes one to three minutes.
|
|
26
29
|
|
|
30
|
+
Connect a separate Langfuse Viewer key through CLI or the optional web UI (see
|
|
31
|
+
openmerit connect langfuse --help), then --sync langfuse refreshes mapped, tagged
|
|
32
|
+
experiment aggregates before recommending.
|
|
33
|
+
Results are stored per user across conversations for 90 days. Disconnect deletes
|
|
34
|
+
the key and imports; use Delete all evaluation data to remove reported results too.
|
|
35
|
+
Past conversations remain. Never put record IDs or emails in source or scope.
|
|
36
|
+
|
|
27
37
|
A new conversation is limited to the endpoint IDs in ${policyFile}
|
|
28
38
|
(set with openmerit project). --all-endpoints searches the whole catalogue and is
|
|
29
39
|
refused under the existing-only policy. Uses the device key that
|
|
@@ -177,6 +187,11 @@ export async function main(args, { root = process.cwd(), print = console.log } =
|
|
|
177
187
|
request.allEndpoints = true;
|
|
178
188
|
else if (arg === '--json')
|
|
179
189
|
json = true;
|
|
190
|
+
else if (arg === '--sync') {
|
|
191
|
+
if (value() !== 'langfuse')
|
|
192
|
+
throw new Error('--sync supports langfuse.');
|
|
193
|
+
request.sync = 'langfuse';
|
|
194
|
+
}
|
|
180
195
|
else if (arg === '--results') {
|
|
181
196
|
const file = value();
|
|
182
197
|
let results;
|
|
@@ -190,7 +205,9 @@ export async function main(args, { root = process.cwd(), print = console.log } =
|
|
|
190
205
|
throw new Error(`${file} must hold a JSON list of results.`);
|
|
191
206
|
// Scout accepts at most 50 per turn; each call is a turn that recommends again, so splitting is the caller's choice.
|
|
192
207
|
if (results.length > 50)
|
|
193
|
-
throw new Error(`${file} holds ${results.length} results; Scout accepts at most 50 per call
|
|
208
|
+
throw new Error(`${file} holds ${results.length} results; Scout accepts at most 50 per call. Split them into files of up to 50 and report each with --results in turn; Scout recommends again after each.`);
|
|
209
|
+
if (results.some(r => isObject(r) && [r.source, r.scope].some(v => typeof v === 'string' && /(?:\b(?:claim|patient|member|FICT|CLM)[-_ ]?\d|\b[0-9a-f]{8}-[0-9a-f-]{27,}\b|[^\s@]+@[^\s@]+\.[^\s@]+)/i.test(v))))
|
|
210
|
+
print('Warning: source or scope looks like a record ID or email. Send aggregate labels only; remove identifiers before sharing.');
|
|
194
211
|
request.results = results;
|
|
195
212
|
}
|
|
196
213
|
else if (arg.startsWith('-'))
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function safeFrames(error: any): {
|
|
2
|
+
filename: string | undefined;
|
|
3
|
+
lineno: number;
|
|
4
|
+
colno: number;
|
|
5
|
+
}[];
|
|
6
|
+
export function errorProperties(error: any): {
|
|
7
|
+
error_type: any;
|
|
8
|
+
frames: {
|
|
9
|
+
filename: string | undefined;
|
|
10
|
+
lineno: number;
|
|
11
|
+
colno: number;
|
|
12
|
+
}[];
|
|
13
|
+
};
|
|
14
|
+
export function setupHeaders(ids?: {}, client?: string): {
|
|
15
|
+
'X-Scout-Installation-Id'?: any;
|
|
16
|
+
'X-Scout-Setup-Id'?: any;
|
|
17
|
+
'X-Scout-Analytics-Account'?: string;
|
|
18
|
+
'X-Scout-Analytics'?: string;
|
|
19
|
+
'X-Scout-Analytics-Content': string;
|
|
20
|
+
'X-Scout-Client-Version'?: any;
|
|
21
|
+
'X-Scout-Client': string;
|
|
22
|
+
'X-Scout-Agent': string;
|
|
23
|
+
};
|
|
24
|
+
export function installationProperties(input: any): {} | null;
|
|
25
|
+
export function sendInstallationEvent(input: any): Promise<void>;
|
|
26
|
+
export function commandTelemetry(args: any): (outcome: any, error: any, code: any) => Promise<void>;
|
|
27
|
+
export let clientVersion: any;
|
|
28
|
+
export function telemetryEnabled(defaultEnabled?: boolean): boolean;
|
|
29
|
+
export function setupCorrelationEnabled(): boolean;
|
|
30
|
+
export function setupUUID(value: any): boolean;
|
|
@@ -7,7 +7,7 @@ import { mkdir, open, readFile, rename, rm, stat, writeFile } from 'node:fs/prom
|
|
|
7
7
|
import { dirname, join, resolve } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { gunzipSync } from 'node:zlib';
|
|
10
|
-
export const updateContract = 'scout-recommend-
|
|
10
|
+
export const updateContract = 'scout-recommend-v2';
|
|
11
11
|
const registry = 'https://registry.npmjs.org/openmerit/latest';
|
|
12
12
|
const day = 24 * 60 * 60 * 1000;
|
|
13
13
|
const retry = 15 * 60 * 1000;
|
package/docs/index.md
CHANGED
|
@@ -16,6 +16,9 @@ Follow [setup](getting-started.md#connect-scout-to-a-coding-agent) and [the Pi g
|
|
|
16
16
|
|
|
17
17
|
Use your own representative task checks to evaluate model candidates. The [standalone evaluation CLI is retired in npm 0.3.0](agent-evaluations.md); existing local artifacts are not deleted.
|
|
18
18
|
|
|
19
|
+
Follow [close the model-selection loop](scout-close-loop.md) to consult, measure,
|
|
20
|
+
report and choose. It distinguishes npm 0.3.4 from the unreleased Langfuse connector.
|
|
21
|
+
|
|
19
22
|
## Operate and troubleshoot
|
|
20
23
|
|
|
21
24
|
[Commands](commands.md) lists the Scout and setup commands. The [Inference Terminal is retired in npm 0.3.0](inference-terminal.md); existing local files are not deleted. [Troubleshooting](troubleshooting.md), [security and data](security.md), and [current limitations](roadmap.md) explain setup failures and product boundaries. [Testing and validation](testing.md) distinguishes automated coverage from product evidence.
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# Close the model-selection loop
|
|
2
|
+
|
|
3
|
+
Scout recommends candidates; your own evals decide whether they work. Never put
|
|
4
|
+
inputs, outputs, patient records, source code or secrets in recommendation prompts
|
|
5
|
+
or tool arguments. Connector credentials use the protected paths described below.
|
|
6
|
+
|
|
7
|
+
**Version requirements:** the Langfuse setup commands below require npm 0.3.5 or
|
|
8
|
+
newer and a matching Scout serving deployment. Saving Langfuse credentials also
|
|
9
|
+
requires connection encryption to be enabled on the server. Publishing the npm
|
|
10
|
+
package alone does not deploy or enable the hosted connector. npm 0.3.4 supports
|
|
11
|
+
reporting aggregates per conversation, not this connector.
|
|
12
|
+
|
|
13
|
+
## Install and consult
|
|
14
|
+
|
|
15
|
+
Install the published package, approve the device code in your browser and start a
|
|
16
|
+
new coding-agent session. See [setup](getting-started.md) for Pi and unattended
|
|
17
|
+
reuse of an already-approved device key. Do not copy another user's key.
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npx -y openmerit@latest install --client claude-code --scope project
|
|
21
|
+
npx -y openmerit@latest scout "Extract invoice line items as structured JSON" --all-endpoints
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Use your saved project inventory instead of `--all-endpoints` when policy limits
|
|
25
|
+
access. Existing-only policy cannot be broadened by that flag. Answer Scout's
|
|
26
|
+
questions in the same conversation with `--brief`, or explicitly choose `--skip`.
|
|
27
|
+
Each recommended model names an exact endpoint and provider model ID.
|
|
28
|
+
|
|
29
|
+
## Measure with your own evals
|
|
30
|
+
|
|
31
|
+
Add the shortlist as runs of your existing evals, not a second platform. Obtain
|
|
32
|
+
approval before model spend or platform writes. For agents, change **one agent at
|
|
33
|
+
a time** while holding the other models, prompts, tools, fixtures and controls
|
|
34
|
+
fixed. Use that agent's own checks, not just end-to-end success, to attribute
|
|
35
|
+
failures. Include cost and latency per call or agent run.
|
|
36
|
+
|
|
37
|
+
If every candidate scores 100%, the eval does not distinguish their quality.
|
|
38
|
+
Add harder labelled boundary cases, rare failures and adversarial examples;
|
|
39
|
+
cheaper on this test is not proof of equal quality in production.
|
|
40
|
+
|
|
41
|
+
## Report aggregates
|
|
42
|
+
|
|
43
|
+
Report up to 50 aggregates per call, one per model, metric and scope. Use public
|
|
44
|
+
catalogue model or endpoint IDs. `scope` and `source` must be aggregate labels,
|
|
45
|
+
not record IDs, emails or URLs containing private identifiers.
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
[
|
|
49
|
+
{
|
|
50
|
+
"level": "agent", "kind": "quality", "task": "Invoice extraction",
|
|
51
|
+
"scope": "Invoice extraction", "model": "<catalogue-model-id>",
|
|
52
|
+
"metric": "fields-correct", "value": 0.96, "higherIsBetter": true,
|
|
53
|
+
"samples": 100, "source": "Held-out invoice eval"
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
openmerit scout --conversation <id> --results results.json
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Use `kind: "usd"` or `"seconds"` for measured money or time; they are not quality
|
|
63
|
+
scores. Once the per-user change is deployed, aggregates apply across your
|
|
64
|
+
conversations. Re-reporting the same level/kind/task/scope/model/metric/source
|
|
65
|
+
updates that aggregate rather than duplicating it.
|
|
66
|
+
|
|
67
|
+
## Connect Langfuse
|
|
68
|
+
|
|
69
|
+
After release, `openmerit connect langfuse` opens the optional signed-in
|
|
70
|
+
[connection page](https://openmerit.ai/connect/?integration=langfuse). Create a
|
|
71
|
+
**separate Viewer key pair** in Langfuse Project Settings → API Keys. Enter it
|
|
72
|
+
in the password fields there, or use the protected headless paths below. Never
|
|
73
|
+
paste it in chat, command arguments, issues or tracked files. Keep the
|
|
74
|
+
application's tracing keys unchanged. US, EU and HIPAA Cloud hosts
|
|
75
|
+
are supported; self-hosted instances are not. Selecting a Langfuse region does
|
|
76
|
+
not promise that Scout stores or processes your data in that region.
|
|
77
|
+
|
|
78
|
+
You can connect the key first with empty mappings. To import results, expand
|
|
79
|
+
**Score and model mappings** and supply a start date, experiment-tag prefix,
|
|
80
|
+
score mappings and model aliases. In npm 0.3.6 with the matching Scout deployment,
|
|
81
|
+
the headless paths below can update settings without re-entering the saved Viewer
|
|
82
|
+
key. Changing settings invalidates old imports; sync again afterwards. Reported
|
|
83
|
+
results remain. Older deployments require disconnecting and reconnecting.
|
|
84
|
+
|
|
85
|
+
### Headless CLI setup
|
|
86
|
+
|
|
87
|
+
First authorize a **separate connection-management key**. This is not an upgrade
|
|
88
|
+
to your existing recommendation key and does not change Claude Code or Pi setup.
|
|
89
|
+
The first account authorization still requires a signed-in person to approve the
|
|
90
|
+
displayed device code; `--no-open` lets them do that on another device. After
|
|
91
|
+
approval, all connector operations can run without the web UI.
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
openmerit connect langfuse --authorize --no-open
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
If it returns exit 10 (approval pending), approve the matching code and rerun the
|
|
98
|
+
same command. The resumable login stores the management key with mode 0600 in
|
|
99
|
+
`connector/user/credentials.json` under the OpenMerit config directory (normally
|
|
100
|
+
`~/.config/openmerit`). Never display or share that file. The key grants
|
|
101
|
+
`scout:connections`, not `scout:recommend`; it can manage **all evaluation data
|
|
102
|
+
for your account**, not only one project. It cannot approve devices, list other
|
|
103
|
+
device keys, run recommendations or spend on models. Like recommendation keys,
|
|
104
|
+
it expires after 90 days without use and can be revoked from the connection page
|
|
105
|
+
or with `openmerit logout --client connector`. Disconnecting Langfuse does not
|
|
106
|
+
revoke this management credential; revoke it separately when retiring a runner.
|
|
107
|
+
|
|
108
|
+
Save only non-secret settings in `mappings.json`:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"region": "us",
|
|
113
|
+
"from": "2026-11-01",
|
|
114
|
+
"tagPrefix": "experiment:",
|
|
115
|
+
"mappings": [],
|
|
116
|
+
"models": {}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Have your secret manager inject `LANGFUSE_PUBLIC_KEY` and `LANGFUSE_SECRET_KEY`
|
|
121
|
+
into the CLI process environment, then run:
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
openmerit connect langfuse --config mappings.json
|
|
125
|
+
openmerit connect langfuse --status
|
|
126
|
+
openmerit connect langfuse --sync
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Updating an existing connection (npm 0.3.6 and matching deployment):** edit the
|
|
130
|
+
non-secret settings file and run the same `--config` command with both
|
|
131
|
+
`LANGFUSE_PUBLIC_KEY` and `LANGFUSE_SECRET_KEY` absent. Scout reuses and reverifies
|
|
132
|
+
the encrypted Viewer key internally; neither CLI nor model receives it. One key
|
|
133
|
+
without the other is rejected. Without a connected key, first connect through a
|
|
134
|
+
protected credential path. Settings changes remove prior imports, not reported
|
|
135
|
+
results, and require a new sync.
|
|
136
|
+
|
|
137
|
+
Alternatively, `openmerit connect langfuse --stdin` reads a full JSON object with
|
|
138
|
+
the same settings plus `publicKey` and `secretKey` from a protected pipe, up to
|
|
139
|
+
18 KB. It does not echo or save the Viewer credentials locally. Do not use inline
|
|
140
|
+
JSON, `echo` with literal secrets, command substitutions that print secrets into
|
|
141
|
+
an agent transcript, or an interactive shell history entry. Authorize before
|
|
142
|
+
piping a secret. Environment variables and pipes are not a vault: trust the
|
|
143
|
+
runner and restrict process/debug access, and disable shell tracing and request
|
|
144
|
+
logging. Unattended jobs may instead inject an already-approved
|
|
145
|
+
`OPENMERIT_CONNECTION_KEY` through a secret manager; keep recommendation and
|
|
146
|
+
management credentials separate.
|
|
147
|
+
|
|
148
|
+
### MCP management
|
|
149
|
+
|
|
150
|
+
The canonical `/scout/mcp` endpoint exposes `scout_connection` when authenticated
|
|
151
|
+
with the separate management key (or a current browser session). Register a
|
|
152
|
+
separate trusted MCP client connection using `Authorization: Bearer <management
|
|
153
|
+
key>` without putting the actual key in chat or tool arguments. A recommendation
|
|
154
|
+
key still discovers only `scout_models`; a management key discovers only
|
|
155
|
+
`scout_connection`. This separation is enforced on calls, not just discovery.
|
|
156
|
+
|
|
157
|
+
`scout_connection` supports `status`, `configure`, `sync`, `disconnect` and
|
|
158
|
+
`clear`. `configure` takes the non-secret settings above as `config`. Supply the
|
|
159
|
+
Viewer key pair through **client-controlled HTTP headers**
|
|
160
|
+
`X-Langfuse-Public-Key` and `X-Langfuse-Secret-Key`, sourced from your secret manager,
|
|
161
|
+
not from the model. These headers are consumed only for configuration and are
|
|
162
|
+
never returned in tool results or sent to the recommendation model. Remove them
|
|
163
|
+
from client configuration after connecting. Do not configure a client/proxy that
|
|
164
|
+
records these headers. If your MCP client cannot securely inject headers outside
|
|
165
|
+
model context, have the agent invoke the CLI with protected input instead, or
|
|
166
|
+
use the web UI; do not fall back to putting credentials in tool arguments.
|
|
167
|
+
|
|
168
|
+
On the matching 0.3.6 deployment, omit both Viewer headers to update an existing
|
|
169
|
+
connection using its saved encrypted key. A new connection still needs both.
|
|
170
|
+
The authenticated HTTP POST similarly accepts non-secret settings without either
|
|
171
|
+
key field for an existing connection. The saved key is never exported. Settings
|
|
172
|
+
changes reverify Viewer access and invalidate imported results before a new sync.
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{"action": "configure", "config": {"region": "us", "from": "2026-11-01", "tagPrefix": "experiment:", "mappings": [], "models": {}}}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
This checks and saves the connection, not a sync. Send `{"action":"sync"}` to
|
|
179
|
+
import mapped aggregates without starting inference. For deletion, obtain the
|
|
180
|
+
user's consent and pass `confirm: true` with `disconnect` or `clear`. Tool
|
|
181
|
+
confirmation is not a substitute for consent in your application.
|
|
182
|
+
|
|
183
|
+
### HTTP API
|
|
184
|
+
|
|
185
|
+
The same authenticated account store is available without MCP:
|
|
186
|
+
|
|
187
|
+
| Method | Path | Operation |
|
|
188
|
+
| --- | --- | --- |
|
|
189
|
+
| GET | `/connect/api/langfuse` | Connection status and aggregate counts, never credentials |
|
|
190
|
+
| POST | `/connect/api/langfuse` | Configure/replace using full JSON settings including `publicKey` and `secretKey` |
|
|
191
|
+
| POST | `/connect/api/langfuse/sync` | Import mapped aggregates without inference |
|
|
192
|
+
| DELETE | `/connect/api/langfuse` | Delete the Viewer key pair and imported aggregates |
|
|
193
|
+
| DELETE | `/connect/api/evaluations` | Also delete manually reported aggregates |
|
|
194
|
+
|
|
195
|
+
Send the management key in the `Authorization: Bearer` header and
|
|
196
|
+
`Content-Type: application/json` for configuration. Browser session tokens still
|
|
197
|
+
work. Recommendation keys, anonymous requests and cross-origin browser requests
|
|
198
|
+
are rejected. No request accepts a target user ID: ownership comes from the
|
|
199
|
+
authenticated identity. Inject credentials into the HTTP client, not command-line
|
|
200
|
+
arguments; keep request bodies/headers out of logs. Check status after an ambiguous
|
|
201
|
+
network failure before retrying a change. None of these paths bypasses the Viewer
|
|
202
|
+
check, explicit mappings, encryption, retention limits or per-user isolation.
|
|
203
|
+
|
|
204
|
+
### Attribute imported scores
|
|
205
|
+
|
|
206
|
+
Every scored trace must carry a tag formed from the prefix plus its experiment
|
|
207
|
+
ID, for example `experiment:<id>`. The experiment's metadata must identify the
|
|
208
|
+
model for that agent. Scout does not infer model attribution from timestamps,
|
|
209
|
+
trace contents or the currently selected model. Untagged historical runs are
|
|
210
|
+
not imported: report their aggregates instead. Each mapping specifies an exact
|
|
211
|
+
trace name so other agents in the same experiment cannot contaminate its score.
|
|
212
|
+
|
|
213
|
+
Example score mapping (replace the task title with the exact Scout task):
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
[
|
|
217
|
+
{
|
|
218
|
+
"metric": "coding-finding-found", "modelKey": "model_coding",
|
|
219
|
+
"task": "Coding review", "traceName": "review-coding",
|
|
220
|
+
"level": "agent", "kind": "quality", "higherIsBetter": true
|
|
221
|
+
}
|
|
222
|
+
]
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Model aliases explicitly map metadata values to Scout model or endpoint IDs:
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
{"<experiment metadata model value>": "<Scout catalogue model or endpoint ID>"}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The first import supports numeric/boolean score averages and counts, not strings,
|
|
232
|
+
raw scores, percentile distributions or automatic cost/latency extraction from
|
|
233
|
+
generation bodies. At most eight mappings, 100 aliases and 100 experiments are
|
|
234
|
+
read per sync. Narrow the date if a response or import exceeds its limit. Only
|
|
235
|
+
experiment headers/metadata and aggregate Metrics v2 reads are requested. A
|
|
236
|
+
deliberately invalid dataset-create request checks read-only permission without
|
|
237
|
+
ever sending a valid write payload. Only an authenticated read followed by HTTP
|
|
238
|
+
403 on that probe is accepted; other responses fail closed. Live Viewer-key
|
|
239
|
+
connection acceptance was verified on 6 November 2026; fresh mapped aggregate
|
|
240
|
+
import and reranking remain separate release checks.
|
|
241
|
+
|
|
242
|
+
Click **Sync aggregates**, or request a fresh recommendation with:
|
|
243
|
+
|
|
244
|
+
```sh
|
|
245
|
+
openmerit scout --conversation <id> --sync langfuse
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
MCP uses `sync: "langfuse"`. Successful reads are cached for ten minutes; a
|
|
249
|
+
failed read cannot be retried for one minute. A failed import leaves the previous
|
|
250
|
+
complete set intact. `conversationId` alone reads a saved answer, not a rerank;
|
|
251
|
+
send a follow-up or `sync` to recompute with the current results.
|
|
252
|
+
|
|
253
|
+
### When a sync imports zero aggregates
|
|
254
|
+
|
|
255
|
+
A connected Viewer key and a successful experiment read do not establish that
|
|
256
|
+
score import is configured. Empty mappings are allowed when first connecting.
|
|
257
|
+
Before repeating sync, check the score mappings and model aliases, then check
|
|
258
|
+
that scored traces carry the exact experiment tag, trace name and metric name
|
|
259
|
+
in those mappings. Merely linking a trace through Langfuse's native experiment
|
|
260
|
+
attributes does not add the explicit experiment tag required by this connector.
|
|
261
|
+
Do not remove the tag filter to make an import nonempty: that can combine scores
|
|
262
|
+
from different models or runs. Untagged historical results need attributable
|
|
263
|
+
aggregate reporting or a separately approved, verified tagging backfill.
|
|
264
|
+
|
|
265
|
+
**Diagnostic version requirement:** a matching Scout serving deployment adds
|
|
266
|
+
`mappingCount` and `modelAliasCount` to connection status, without returning the
|
|
267
|
+
saved keys or mapping values. The web page distinguishes a connected key from
|
|
268
|
+
incomplete setup and warns when a sync imports no matching scores. npm 0.3.5
|
|
269
|
+
does not display these new counts; use authenticated API/MCP status until a
|
|
270
|
+
matching CLI patch is published. These counts diagnose missing configuration,
|
|
271
|
+
not whether matching scores exist or whether a model improves quality.
|
|
272
|
+
|
|
273
|
+
## Choose and delete
|
|
274
|
+
|
|
275
|
+
Check measured quality, the quality floor, price calibration and limitations
|
|
276
|
+
before adopting a model. Benchmark estimates are not application acceptance.
|
|
277
|
+
Test endpoint connectivity and application behavior separately.
|
|
278
|
+
|
|
279
|
+
Results belong only to your Scout user account, not other customers. Up to 5,000
|
|
280
|
+
aggregates or 1 MB are retained for 90 days from report/import; expired rows are
|
|
281
|
+
excluded immediately and pruned daily (allow scheduling delay). An evaluation
|
|
282
|
+
account inactive for 90 days is erased by the same cleanup. See the
|
|
283
|
+
[data policy](scout-data-policy.md) for processors and backup limitations.
|
|
284
|
+
|
|
285
|
+
`openmerit disconnect langfuse` opens the optional browser confirmation;
|
|
286
|
+
`openmerit disconnect langfuse --yes` deletes headlessly, and adding
|
|
287
|
+
`--delete-all` also deletes reported results. **Disconnect &
|
|
288
|
+
delete imports** removes the encrypted key pair and imported aggregates, including
|
|
289
|
+
an in-flight sync. **Delete all evaluation data** also removes manually reported
|
|
290
|
+
aggregates. Neither erases past conversation text or previously generated
|
|
291
|
+
recommendations; do not keep treating those answers as current measurements.
|
|
292
|
+
Contact the address in the policy for broader deletion.
|
|
293
|
+
|
|
294
|
+
This release changes the installed instruction-data contract. Existing automatic
|
|
295
|
+
updaters stop at that boundary; explicitly reinstall to adopt the new instructions.
|
|
@@ -25,6 +25,53 @@ Setup analytics defaults on. Set `OPENMERIT_TELEMETRY=0` or `DO_NOT_TRACK=1` to
|
|
|
25
25
|
|
|
26
26
|
Turning sharing off stops future optional collection. It does not stop required processing or delete stored data.
|
|
27
27
|
|
|
28
|
+
## Evaluation connections
|
|
29
|
+
|
|
30
|
+
These features require a matching Scout serving deployment; saving Langfuse
|
|
31
|
+
credentials also requires a dedicated server encryption secret. The npm package
|
|
32
|
+
does not deploy or enable the hosted connector. On enabled deployments, reported
|
|
33
|
+
and imported aggregates are held per authenticated user
|
|
34
|
+
in Cloudflare Durable Objects and inform only that user's recommendations across
|
|
35
|
+
conversations. No cross-customer sharing is enabled. Each aggregate records level,
|
|
36
|
+
kind (quality, USD or seconds), task/scope, catalogue model/endpoint ID, metric,
|
|
37
|
+
average/value, sample count, direction, source label and report/import time.
|
|
38
|
+
Do not put record IDs, emails or sensitive information in labels; identifier-like
|
|
39
|
+
source/scope labels trigger warnings, not guaranteed redaction.
|
|
40
|
+
|
|
41
|
+
Connecting Langfuse sends a separate Viewer key pair and mappings from the
|
|
42
|
+
signed-in browser, headless CLI, authenticated API or trusted MCP client to Scout,
|
|
43
|
+
encrypted at rest with a dedicated Worker secret. Headless management uses a
|
|
44
|
+
separately approved connection-management key; existing recommendation keys do
|
|
45
|
+
not gain that permission. The CLI reads Viewer credentials from a protected stdin
|
|
46
|
+
pipe or secret-manager-injected environment, not command arguments, and does not
|
|
47
|
+
save those Viewer credentials locally. MCP accepts them through client-controlled
|
|
48
|
+
HTTP headers, not model-visible tool arguments. Keep client, proxy and shell logs
|
|
49
|
+
from recording credentials. The management key is stored separately on your
|
|
50
|
+
device until revoked/deleted and can delete all evaluation data for your account;
|
|
51
|
+
disconnecting Langfuse does not revoke it. First-time device authorization still
|
|
52
|
+
requires account sign-in and approval. The web connection UI remains optional.
|
|
53
|
+
Scout requests only experiment headers/metadata and aggregate metric averages
|
|
54
|
+
and counts. It does not request inputs, outputs, trace/observation bodies,
|
|
55
|
+
experiment items, raw scores or score comments. Arbitrary metadata is discarded;
|
|
56
|
+
experiment source labels are hashed. A deliberately invalid write-permission
|
|
57
|
+
probe does not send a valid dataset or create traces. Your Langfuse region does
|
|
58
|
+
not constrain Scout's storage or AI-provider region.
|
|
59
|
+
|
|
60
|
+
At most 5,000 aggregates or 1 MB are retained per user. Rows expire 90 days after
|
|
61
|
+
report/import, are excluded from recommendations at expiry and pruned daily;
|
|
62
|
+
cleanup may be delayed by platform scheduling. The evaluation account, including
|
|
63
|
+
its encrypted connection, is erased after 90 days without a report, connection
|
|
64
|
+
or successful sync. Backups and infrastructure logs follow provider policies;
|
|
65
|
+
immediate backup erasure has not been independently verified.
|
|
66
|
+
|
|
67
|
+
On the signed-in [connection page](/connect/?integration=langfuse), disconnecting
|
|
68
|
+
deletes the key and imported aggregates, including imports from an in-flight
|
|
69
|
+
sync. **Delete all evaluation data** also deletes reported aggregates. Past
|
|
70
|
+
conversation text and recommendations remain and may mention prior results;
|
|
71
|
+
request broader deletion through the contact below. Legacy per-conversation
|
|
72
|
+
aggregates migrate when that conversation next receives a turn. Old conversations
|
|
73
|
+
not reopened require the broader deletion request.
|
|
74
|
+
|
|
28
75
|
## Storage and retention
|
|
29
76
|
|
|
30
77
|
- Your browser stores sign-in information, sharing preferences and temporary task drafts.
|
package/docs/scout.md
CHANGED
|
@@ -33,6 +33,7 @@ OpenMerit does not run evaluations or host an eval platform. Scout tells you wha
|
|
|
33
33
|
- **Measured cost.** A result can set `kind` to `usd` or `seconds` (default `quality`): cost or time per model call at the task level, per agent run at the agent level, per outcome at the environment level. Cost and time results are never quality evidence. A model's agent- or task-level `usd` result on a task appears as `cost.measured` (`usdPer1kTasks`, `usdPer1kAgentRuns`, and the `result` it came from), and when every model in a group (measured or unmeasured) has one, the group is ranked by measured cost instead of Scout's catalogue estimate, which can be far off for agent runs.
|
|
34
34
|
- **Environment results and attribution.** An environment-level result also measures every other agent and control. Scout no longer drops a candidate below the bar when its own task- and agent-level quality results on that task match a candidate that reaches it; an environment miss alone does not show the swapped model caused it.
|
|
35
35
|
- **Since the Scout serving deploy of 5 November 2026 (version `a4d46878-5c00-4997-b7e6-0d0abd0187e7`) and npm 0.3.4:** models that tie under the conversation's priority, such as equal quality from the same measured results, are ordered by measured cost when both have one, so a quality priority no longer keeps the benchmark order among measured equals. Results stored before `kind` existed count as quality again, so older conversations keep `quality.measured`. `openmerit scout --results` refuses more than 50 results per call before sending them.
|
|
36
|
+
- **Unreleased source (needs a Scout serving deploy):** the per-task workload is per model call, and Scout's instructions now say so, along with what one agent call usually sends and returns; before, the prompt asked for a per-run workload that was then multiplied by the calls per run. When you report measured cost on a task, every fully priced estimate in that task is scaled by the median ratio of measured to estimated cost among the models you measured, shown as `cost.calibration` (`factor` and the `results` it used); the order among estimates does not change. Catalogue prices still exclude prompt caching, so uncalibrated estimates for agents that reuse a cached prompt stay high. When any shortlisted model has measured cost, only measured cost earns the cheapest label (`best: ["cost"]`), so models that share one catalogue route price no longer all receive it.
|
|
36
37
|
- **Scope.** Results are stored with the conversation for your account and inform only your recommendations. They are not shared with other accounts or used to change Scout's catalogue.
|
|
37
38
|
|
|
38
39
|
The quality bar now considers what an error costs once your environment's controls, such as a person approving every payment, have acted. Cost remains the default ranking among models that reach the bar.
|
package/docs/security.md
CHANGED
|
@@ -12,6 +12,32 @@ OpenMerit accepts no model-provider or Jev credential. Credentials belong to the
|
|
|
12
12
|
|
|
13
13
|
Never place secrets in evaluation configuration, task fixtures, Scout project policy, traces, or committed environment files.
|
|
14
14
|
|
|
15
|
+
### Langfuse connector
|
|
16
|
+
|
|
17
|
+
The [connection guide](scout-close-loop.md#connect-langfuse) requires npm 0.3.5
|
|
18
|
+
and the matching hosted deployment. Only a separate
|
|
19
|
+
Viewer key pair is accepted. A successful read plus a deliberately invalid
|
|
20
|
+
dataset-create permission probe (HTTP 403 required) fails closed for Full keys;
|
|
21
|
+
no valid write payload is sent. Live Viewer connection acceptance was verified
|
|
22
|
+
on 6 November 2026; fresh mapped import/reranking remain separate release gates.
|
|
23
|
+
Fixed US/EU/HIPAA hosts, manual redirects, timeouts and bounded reads
|
|
24
|
+
prevent credential forwarding and unbounded response bodies. The connector reads
|
|
25
|
+
only experiment headers/metadata and aggregate Metrics v2 values, never trace,
|
|
26
|
+
observation, experiment-item or raw-score bodies. Extra metadata fields are
|
|
27
|
+
discarded; experiment source labels are hashes, not names or record IDs.
|
|
28
|
+
|
|
29
|
+
Credentials and mappings are AES-GCM encrypted in one Cloudflare Durable Object
|
|
30
|
+
per authenticated user, bound to that object's ID with a dedicated Worker secret.
|
|
31
|
+
Keys never enter browser storage, URLs or analytics. Keep that encryption secret
|
|
32
|
+
stable: replacing it invalidates stored connections and requires reconnecting.
|
|
33
|
+
With npm 0.3.6 and the matching deployment, authenticated settings-only updates
|
|
34
|
+
reuse and reverify the saved encrypted key without exporting it. Supplying a
|
|
35
|
+
partial key pair fails; first connection still requires a protected Viewer pair.
|
|
36
|
+
Changing settings invalidates imported aggregates and requires syncing again.
|
|
37
|
+
Disconnect deletes the key and imported aggregates; deleting all evaluation data
|
|
38
|
+
also deletes reported aggregates. Neither erases conversation text. Retention,
|
|
39
|
+
limits and processor caveats are in the [data policy](scout-data-policy.md).
|
|
40
|
+
|
|
15
41
|
## Local state
|
|
16
42
|
|
|
17
43
|
`.openmerit/` may hold evaluation configuration, task fixtures, reports and budget usage, and Scout project policy, plus files left by the retired Inference Terminal (`terminal.json`, `inference.jsonl`), which upgrades do not delete. It may reveal task names, model choices, budgets or endpoint identifiers. Treat it as sensitive project metadata even though it must not contain credentials.
|