@meetless/mla 0.1.4
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/LICENSE +201 -0
- package/README.md +81 -0
- package/dist/build-info.json +9 -0
- package/dist/bundles/ask-core.js +396 -0
- package/dist/bundles/mcp.js +16592 -0
- package/dist/bundles/trace-core.js +263 -0
- package/dist/cli.js +828 -0
- package/dist/commands/activate.js +781 -0
- package/dist/commands/adoption.js +130 -0
- package/dist/commands/ask.js +290 -0
- package/dist/commands/context.js +114 -0
- package/dist/commands/debug.js +313 -0
- package/dist/commands/doctor.js +1021 -0
- package/dist/commands/enrich.js +427 -0
- package/dist/commands/evidence.js +229 -0
- package/dist/commands/flush.js +184 -0
- package/dist/commands/graph.js +104 -0
- package/dist/commands/init.js +272 -0
- package/dist/commands/internal-active-review.js +322 -0
- package/dist/commands/internal-auto-index.js +188 -0
- package/dist/commands/internal-capture-decisions.js +320 -0
- package/dist/commands/internal-evidence-correlate.js +239 -0
- package/dist/commands/internal-evidence-hooks.js +240 -0
- package/dist/commands/internal-evidence-inject.js +231 -0
- package/dist/commands/internal-finalize.js +221 -0
- package/dist/commands/internal-pretool-observe.js +225 -0
- package/dist/commands/internal-refresh.js +136 -0
- package/dist/commands/internal-session-nudge.js +120 -0
- package/dist/commands/internal-steer-sync.js +117 -0
- package/dist/commands/internal-turn-recap.js +140 -0
- package/dist/commands/kb.js +375 -0
- package/dist/commands/kb_add.js +681 -0
- package/dist/commands/kb_forget.js +283 -0
- package/dist/commands/kb_move.js +45 -0
- package/dist/commands/kb_pending.js +410 -0
- package/dist/commands/kb_personal.js +149 -0
- package/dist/commands/kb_promote.js +188 -0
- package/dist/commands/kb_purge.js +168 -0
- package/dist/commands/kb_reingest.js +335 -0
- package/dist/commands/kb_retime.js +170 -0
- package/dist/commands/kb_review.js +391 -0
- package/dist/commands/kb_revision.js +179 -0
- package/dist/commands/kb_show.js +385 -0
- package/dist/commands/label.js +226 -0
- package/dist/commands/login.js +295 -0
- package/dist/commands/logout.js +108 -0
- package/dist/commands/mcp-supervisor.js +93 -0
- package/dist/commands/mcp.js +227 -0
- package/dist/commands/queue-prune.js +98 -0
- package/dist/commands/review.js +358 -0
- package/dist/commands/rewire.js +124 -0
- package/dist/commands/rules.js +728 -0
- package/dist/commands/scan-context.js +67 -0
- package/dist/commands/session.js +347 -0
- package/dist/commands/stats.js +479 -0
- package/dist/commands/status.js +61 -0
- package/dist/commands/summary.js +250 -0
- package/dist/commands/turn.js +114 -0
- package/dist/commands/uninstall.js +222 -0
- package/dist/commands/whoami.js +102 -0
- package/dist/commands/workspace.js +130 -0
- package/dist/hooks-template/ce0-post-tool-use.sh +34 -0
- package/dist/hooks-template/ce0-session-start.sh +49 -0
- package/dist/hooks-template/ce0-stop.sh +29 -0
- package/dist/hooks-template/ce0-user-prompt-submit.sh +38 -0
- package/dist/hooks-template/common.sh +934 -0
- package/dist/hooks-template/event-batch-filter.jq +67 -0
- package/dist/hooks-template/flush.sh +503 -0
- package/dist/hooks-template/post-tool-use.sh +423 -0
- package/dist/hooks-template/pre-tool-use.sh +69 -0
- package/dist/hooks-template/session-start.sh +140 -0
- package/dist/hooks-template/stop.sh +308 -0
- package/dist/hooks-template/user-prompt-submit.sh +1162 -0
- package/dist/lib/activation.js +79 -0
- package/dist/lib/active-conflict-cache.js +141 -0
- package/dist/lib/active-memory.js +59 -0
- package/dist/lib/active-review-runner.js +26 -0
- package/dist/lib/agent-decision/index.js +25 -0
- package/dist/lib/agent-decision/keys.js +49 -0
- package/dist/lib/agent-decision/normalize-claude.js +183 -0
- package/dist/lib/agent-decision/types.js +21 -0
- package/dist/lib/agent-decision/validate.js +216 -0
- package/dist/lib/analytics/capture.js +96 -0
- package/dist/lib/analytics/command-event.js +267 -0
- package/dist/lib/analytics/consent.js +58 -0
- package/dist/lib/analytics/coverage-gap.js +96 -0
- package/dist/lib/analytics/envelope.js +236 -0
- package/dist/lib/analytics/event-id.js +86 -0
- package/dist/lib/analytics/evidence.js +150 -0
- package/dist/lib/analytics/followthrough.js +194 -0
- package/dist/lib/analytics/forwarder.js +109 -0
- package/dist/lib/analytics/logs.js +78 -0
- package/dist/lib/analytics/metrics.js +78 -0
- package/dist/lib/analytics/recorder.js +92 -0
- package/dist/lib/analytics/review-analytics.js +75 -0
- package/dist/lib/analytics/sequence.js +77 -0
- package/dist/lib/analytics/store.js +131 -0
- package/dist/lib/analytics/turn-recap.js +279 -0
- package/dist/lib/artifact_id.js +108 -0
- package/dist/lib/auth-breaker.js +161 -0
- package/dist/lib/auto-index.js +112 -0
- package/dist/lib/classifier.js +88 -0
- package/dist/lib/config.js +298 -0
- package/dist/lib/conflict-advisory.js +64 -0
- package/dist/lib/debug-bundle.js +520 -0
- package/dist/lib/enrichment/ingest.js +301 -0
- package/dist/lib/enrichment/plan.js +253 -0
- package/dist/lib/enrichment/protocol.js +359 -0
- package/dist/lib/enrichment/scout-brief.js +176 -0
- package/dist/lib/failure-telemetry.js +444 -0
- package/dist/lib/git.js +200 -0
- package/dist/lib/governance-cache.js +77 -0
- package/dist/lib/governed-path-cache.js +76 -0
- package/dist/lib/http.js +677 -0
- package/dist/lib/identity-envelope.js +23 -0
- package/dist/lib/kb-candidate.js +65 -0
- package/dist/lib/kb_acl.js +98 -0
- package/dist/lib/login.js +353 -0
- package/dist/lib/mcp-fetchers.js +130 -0
- package/dist/lib/mcp-restart.js +47 -0
- package/dist/lib/observability.js +805 -0
- package/dist/lib/open-url.js +33 -0
- package/dist/lib/orphan-guard.js +70 -0
- package/dist/lib/packaged.js +21 -0
- package/dist/lib/reconcile-sessions.js +171 -0
- package/dist/lib/redactor.js +89 -0
- package/dist/lib/relationship-candidate-query.js +27 -0
- package/dist/lib/render.js +611 -0
- package/dist/lib/rules/applicability.js +64 -0
- package/dist/lib/rules/attest-code-rule-version.js +47 -0
- package/dist/lib/rules/attest-notes-location.js +217 -0
- package/dist/lib/rules/attest-rule-version.js +69 -0
- package/dist/lib/rules/canonical-json.js +97 -0
- package/dist/lib/rules/ce0-emit.js +64 -0
- package/dist/lib/rules/ce0-evidence.js +281 -0
- package/dist/lib/rules/ce0-recall-sample.js +82 -0
- package/dist/lib/rules/ce0-rule.js +55 -0
- package/dist/lib/rules/ce0-sampling-bucket.js +15 -0
- package/dist/lib/rules/ce0-store.js +683 -0
- package/dist/lib/rules/ce0-telemetry-project.js +93 -0
- package/dist/lib/rules/ce0-telemetry.js +158 -0
- package/dist/lib/rules/code-rule-registry.js +17 -0
- package/dist/lib/rules/command-match.js +185 -0
- package/dist/lib/rules/consult-evidence-binding.js +27 -0
- package/dist/lib/rules/consultation-capture-adapter.js +193 -0
- package/dist/lib/rules/content-match.js +56 -0
- package/dist/lib/rules/deny-admission.js +99 -0
- package/dist/lib/rules/durable-observation.js +190 -0
- package/dist/lib/rules/enforce-notes-version.js +421 -0
- package/dist/lib/rules/evaluation-input-hash.js +126 -0
- package/dist/lib/rules/evaluator.js +108 -0
- package/dist/lib/rules/inert-rule-families.js +51 -0
- package/dist/lib/rules/input-authority-resolver.js +241 -0
- package/dist/lib/rules/interception-schema.js +170 -0
- package/dist/lib/rules/interception-store.js +267 -0
- package/dist/lib/rules/live-input-authority.js +66 -0
- package/dist/lib/rules/local-matcher.js +108 -0
- package/dist/lib/rules/local-observe.js +79 -0
- package/dist/lib/rules/local-rule-version-repo.js +214 -0
- package/dist/lib/rules/memory-requirement.js +109 -0
- package/dist/lib/rules/notes-observe.js +39 -0
- package/dist/lib/rules/notes-path.js +261 -0
- package/dist/lib/rules/notes-rule.js +75 -0
- package/dist/lib/rules/observe-adapter.js +114 -0
- package/dist/lib/rules/observed-rule-hash.js +119 -0
- package/dist/lib/rules/prompt-submit-adapter.js +132 -0
- package/dist/lib/rules/requirement-subject.js +240 -0
- package/dist/lib/rules/rule-activity.js +67 -0
- package/dist/lib/rules/rule-version-hash.js +151 -0
- package/dist/lib/rules/runtime-scope.js +55 -0
- package/dist/lib/rules/stop-adapter.js +116 -0
- package/dist/lib/rules/stop-response-snapshot.js +174 -0
- package/dist/lib/rules/types.js +10 -0
- package/dist/lib/rules/ulid.js +46 -0
- package/dist/lib/rules/version-evaluation.js +156 -0
- package/dist/lib/scanner/agent-memory.js +99 -0
- package/dist/lib/scanner/bootstrap-summary.js +87 -0
- package/dist/lib/scanner/cache.js +59 -0
- package/dist/lib/scanner/frontmatter.js +42 -0
- package/dist/lib/scanner/parse-directives.js +69 -0
- package/dist/lib/scanner/parse-structured.js +72 -0
- package/dist/lib/scanner/render.js +73 -0
- package/dist/lib/scanner/scan.js +132 -0
- package/dist/lib/scanner/score.js +38 -0
- package/dist/lib/scanner/scout-mission.js +126 -0
- package/dist/lib/scanner/types.js +7 -0
- package/dist/lib/session-scope.js +195 -0
- package/dist/lib/spool.js +355 -0
- package/dist/lib/staleness.js +100 -0
- package/dist/lib/steer-cache.js +87 -0
- package/dist/lib/tagged-reference.js +20 -0
- package/dist/lib/temporal.js +109 -0
- package/dist/lib/turn-recap-emit.js +67 -0
- package/dist/lib/unwire.js +253 -0
- package/dist/lib/update-check.js +469 -0
- package/dist/lib/update-notifier.js +217 -0
- package/dist/lib/upgrade-apply.js +643 -0
- package/dist/lib/wire.js +1087 -0
- package/dist/lib/workspace.js +96 -0
- package/dist/lib/zip.js +154 -0
- package/dist/pretool-entry.js +37 -0
- package/package.json +75 -0
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// mla observability spine: trace_id + Sentry helpers + build-info loader.
|
|
3
|
+
//
|
|
4
|
+
// See notes/20260530-mla-observability-diagnostic-spine.md for the full design.
|
|
5
|
+
// One trace_id per mla run; same string used as Sentry tag, X-Trace-ID header,
|
|
6
|
+
// and Langfuse trace id once intel propagates it.
|
|
7
|
+
//
|
|
8
|
+
// Canonical format is 32 hex chars (16 random bytes hex-encoded). This is OTel-
|
|
9
|
+
// native and matches intel's existing RequestContext.langfuse_trace_id format.
|
|
10
|
+
// Spec (§4) names UUIDv4, but every Langfuse SDK and intel itself wants 32-hex
|
|
11
|
+
// without dashes; zero translation across planes is the prize.
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.TRACE_FLUSH_CEILING_MS = exports.HTTP_FLUSH_TIMEOUT_MS = exports.TraceRoundTripError = exports.telemetryDisabled = void 0;
|
|
47
|
+
exports.loadBuildInfo = loadBuildInfo;
|
|
48
|
+
exports.mintTraceId = mintTraceId;
|
|
49
|
+
exports.isCanonicalTraceId = isCanonicalTraceId;
|
|
50
|
+
exports.canonicalizeSessionId = canonicalizeSessionId;
|
|
51
|
+
exports.redactSentryEvent = redactSentryEvent;
|
|
52
|
+
exports.initSentry = initSentry;
|
|
53
|
+
exports.isSentryAvailable = isSentryAvailable;
|
|
54
|
+
exports.setWorkspaceConfig = setWorkspaceConfig;
|
|
55
|
+
exports.getWorkspaceConfig = getWorkspaceConfig;
|
|
56
|
+
exports.workspaceSentryAllowed = workspaceSentryAllowed;
|
|
57
|
+
exports.pickSafeObservabilityFields = pickSafeObservabilityFields;
|
|
58
|
+
exports.setSafeObservabilityContext = setSafeObservabilityContext;
|
|
59
|
+
exports.captureCliError = captureCliError;
|
|
60
|
+
exports.captureCliNonZeroExit = captureCliNonZeroExit;
|
|
61
|
+
exports.captureBootstrapError = captureBootstrapError;
|
|
62
|
+
exports.boundedSentryFlush = boundedSentryFlush;
|
|
63
|
+
exports.setRunTraceId = setRunTraceId;
|
|
64
|
+
exports.getRunTraceId = getRunTraceId;
|
|
65
|
+
exports.setRunSessionId = setRunSessionId;
|
|
66
|
+
exports.getRunSessionId = getRunSessionId;
|
|
67
|
+
exports.resetRunSessionIdForTesting = resetRunSessionIdForTesting;
|
|
68
|
+
exports.mintRunId = mintRunId;
|
|
69
|
+
exports.setRunId = setRunId;
|
|
70
|
+
exports.getRunId = getRunId;
|
|
71
|
+
exports.resetRunIdForTesting = resetRunIdForTesting;
|
|
72
|
+
exports.setRepoFingerprint = setRepoFingerprint;
|
|
73
|
+
exports.getRepoFingerprint = getRepoFingerprint;
|
|
74
|
+
exports.resetRepoFingerprintForTesting = resetRepoFingerprintForTesting;
|
|
75
|
+
exports.noteIntelEchoedTraceId = noteIntelEchoedTraceId;
|
|
76
|
+
exports.didIntelEchoTraceId = didIntelEchoTraceId;
|
|
77
|
+
exports.resetIntelEchoForTesting = resetIntelEchoForTesting;
|
|
78
|
+
exports.didTraceFlushSucceed = didTraceFlushSucceed;
|
|
79
|
+
exports.resetTraceFlushOutcomeForTesting = resetTraceFlushOutcomeForTesting;
|
|
80
|
+
exports.makeHttpFlush = makeHttpFlush;
|
|
81
|
+
exports.setRunTracer = setRunTracer;
|
|
82
|
+
exports.getRunTracer = getRunTracer;
|
|
83
|
+
exports.resetRunTracerForTesting = resetRunTracerForTesting;
|
|
84
|
+
exports.routeNameFromPath = routeNameFromPath;
|
|
85
|
+
exports.redactArgvForSpan = redactArgvForSpan;
|
|
86
|
+
exports.langfuseTraceUrl = langfuseTraceUrl;
|
|
87
|
+
exports.langfuseDeepLink = langfuseDeepLink;
|
|
88
|
+
exports.shouldPrintDeepLink = shouldPrintDeepLink;
|
|
89
|
+
exports.maybePrintDeepLink = maybePrintDeepLink;
|
|
90
|
+
exports.createRunTracer = createRunTracer;
|
|
91
|
+
exports.boundedTraceFlush = boundedTraceFlush;
|
|
92
|
+
const crypto = __importStar(require("crypto"));
|
|
93
|
+
const fs = __importStar(require("fs"));
|
|
94
|
+
const path = __importStar(require("path"));
|
|
95
|
+
const Sentry = __importStar(require("@sentry/node"));
|
|
96
|
+
const redactor_1 = require("./redactor");
|
|
97
|
+
const config_1 = require("./config");
|
|
98
|
+
Object.defineProperty(exports, "telemetryDisabled", { enumerable: true, get: function () { return config_1.telemetryDisabled; } });
|
|
99
|
+
const failure_telemetry_1 = require("./failure-telemetry");
|
|
100
|
+
function loadTraceCore() {
|
|
101
|
+
try {
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
103
|
+
return require(path.resolve(__dirname, "..", "bundles", "trace-core.js"));
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
const code = e?.code;
|
|
107
|
+
if (code !== "MODULE_NOT_FOUND" && code !== "ERR_MODULE_NOT_FOUND")
|
|
108
|
+
throw e;
|
|
109
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
110
|
+
return require("@meetless/trace-core");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const { makeNoopTracer, makeTracer } = loadTraceCore();
|
|
114
|
+
let cachedBuildInfo = null;
|
|
115
|
+
function loadBuildInfo() {
|
|
116
|
+
if (cachedBuildInfo)
|
|
117
|
+
return cachedBuildInfo;
|
|
118
|
+
try {
|
|
119
|
+
const raw = fs.readFileSync(path.join(__dirname, "..", "build-info.json"), "utf8");
|
|
120
|
+
cachedBuildInfo = JSON.parse(raw);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
cachedBuildInfo = {
|
|
124
|
+
version: "0.0.0",
|
|
125
|
+
sha: "dev",
|
|
126
|
+
branch: "dev",
|
|
127
|
+
dirty: true,
|
|
128
|
+
builtAt: new Date().toISOString(),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return cachedBuildInfo;
|
|
132
|
+
}
|
|
133
|
+
function mintTraceId() {
|
|
134
|
+
return crypto.randomBytes(16).toString("hex");
|
|
135
|
+
}
|
|
136
|
+
// OBS-1 shape: a canonical trace_id is exactly 32 lowercase hex chars (OTel
|
|
137
|
+
// native, the shape mintTraceId produces and the shape intel adopts verbatim as
|
|
138
|
+
// a Langfuse trace id). The single source of truth for the shape guard, reused
|
|
139
|
+
// by the debug-bundle command's `--trace-id` validation (Phase 5 / gap 6.7) so a
|
|
140
|
+
// malformed id is rejected up front rather than seeding a bad bundle path.
|
|
141
|
+
const CANONICAL_TRACE_ID_RE = /^[0-9a-f]{32}$/;
|
|
142
|
+
function isCanonicalTraceId(value) {
|
|
143
|
+
return typeof value === "string" && CANONICAL_TRACE_ID_RE.test(value);
|
|
144
|
+
}
|
|
145
|
+
// The single canonical Claude agent-session UUID grammar, byte-identical to the
|
|
146
|
+
// Python twin (intel app/observability/langfuse_session.py:_AGENT_SESSION_RE) and
|
|
147
|
+
// the Bash hook twin. NEVER a language UUID parser: Python's uuid.UUID accepts
|
|
148
|
+
// braces, urn:uuid:, and un-dashed 32-hex, and JS has no native UUID parser at
|
|
149
|
+
// all; agreeing on this one explicit regex is what stops the same Claude id
|
|
150
|
+
// canonicalizing to two different strings and splitting the Langfuse Session.
|
|
151
|
+
const AGENT_SESSION_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
152
|
+
// TS twin of canonicalize_agent_session_id. Pure: no metric, no logging. Trim,
|
|
153
|
+
// match the regex (case-insensitive), lowercase; no match -> null. The regex is
|
|
154
|
+
// anchored, so any leftover whitespace or header-injection byte after trim fails
|
|
155
|
+
// the match and yields null, which is what keeps the composed value safe to pass
|
|
156
|
+
// to a `-H` curl header downstream.
|
|
157
|
+
function canonicalizeSessionId(raw) {
|
|
158
|
+
if (raw === null || raw === undefined)
|
|
159
|
+
return null;
|
|
160
|
+
const s = raw.trim();
|
|
161
|
+
return AGENT_SESSION_RE.test(s) ? s.toLowerCase() : null;
|
|
162
|
+
}
|
|
163
|
+
let sentryAvailable = false;
|
|
164
|
+
// Keys whose VALUE is always a credential regardless of entropy, redacted by
|
|
165
|
+
// name in every Sentry event (breadcrumb data, contexts, extra, tags, request
|
|
166
|
+
// headers, exception/stack vars). This is the §9 Sentry-redaction invariant
|
|
167
|
+
// (Finding K / Patch P7): the observability layer must never ship an
|
|
168
|
+
// Authorization header, access/refresh token, PKCE codeVerifier, control token,
|
|
169
|
+
// or INTERNAL_API_KEY off the machine, even with telemetry enabled.
|
|
170
|
+
//
|
|
171
|
+
// Bare `code` is deliberately ABSENT: it collides with error/status/language
|
|
172
|
+
// codes. The one-time login-grant `code` is 64-hex high-entropy, so it is
|
|
173
|
+
// caught by the value-based redactor (entropy heuristic in redact()) instead.
|
|
174
|
+
const SENTRY_SENSITIVE_KEY = /(authorization|access[_-]?token|refresh[_-]?token|code[_-]?verifier|control[_-]?token|internal[_-]?api[_-]?key|\bapi[_-]?key\b|x-api-key|secret|passw(?:or)?d|\bbearer\b|cookie|\btoken\b)/i;
|
|
175
|
+
// Keys carrying NON-secret high-entropy identifiers (trace/span/event/run ids,
|
|
176
|
+
// git sha, build version, environment). These are exactly the strings the value
|
|
177
|
+
// redactor's entropy heuristic would otherwise nuke, and they are the whole
|
|
178
|
+
// point of the observability spine (the cross-plane trace-id join + release
|
|
179
|
+
// correlation). Exempt them from value redaction; everything else high-entropy
|
|
180
|
+
// stays conservatively redacted (over-redaction is the safe failure mode).
|
|
181
|
+
const SENTRY_SAFE_IDENTIFIER_KEY = /^(x-)?(trace|span|event|run)[_-]?id$|^trace_source$|^release$|^dist$|^sha$|^environment$|^mla_version$|^platform$/i;
|
|
182
|
+
// One recursive scrub of a Sentry event. Per field: a credential KEY collapses
|
|
183
|
+
// to [REDACTED]; a safe-identifier key passes its value verbatim; every other
|
|
184
|
+
// string runs through the shared value redactor (Bearer/provider tokens, PEM,
|
|
185
|
+
// cookies, and the high-entropy heuristic that catches the grant code +
|
|
186
|
+
// codeVerifier even when they hide in a free-text body).
|
|
187
|
+
function scrubEventNode(value, keyHint) {
|
|
188
|
+
if (keyHint !== undefined && SENTRY_SENSITIVE_KEY.test(keyHint))
|
|
189
|
+
return redactor_1.REDACTED;
|
|
190
|
+
if (typeof value === "string") {
|
|
191
|
+
if (keyHint !== undefined && SENTRY_SAFE_IDENTIFIER_KEY.test(keyHint))
|
|
192
|
+
return value;
|
|
193
|
+
return (0, redactor_1.redact)(value);
|
|
194
|
+
}
|
|
195
|
+
if (Array.isArray(value))
|
|
196
|
+
return value.map((v) => scrubEventNode(v, keyHint));
|
|
197
|
+
if (value !== null && typeof value === "object") {
|
|
198
|
+
const out = {};
|
|
199
|
+
for (const [k, v] of Object.entries(value)) {
|
|
200
|
+
out[k] = scrubEventNode(v, k);
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
// beforeSend hook: scrub every event before it leaves the process. On any
|
|
207
|
+
// redaction error we DROP the event (return null) rather than risk shipping an
|
|
208
|
+
// unscrubbed payload.
|
|
209
|
+
function redactSentryEvent(event) {
|
|
210
|
+
if (event === null || event === undefined)
|
|
211
|
+
return event;
|
|
212
|
+
try {
|
|
213
|
+
return scrubEventNode(event, undefined);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function initSentry(buildInfo) {
|
|
220
|
+
// 4.4 kill switch wins over everything, including a baked production DSN.
|
|
221
|
+
if ((0, config_1.telemetryDisabled)()) {
|
|
222
|
+
sentryAvailable = false;
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
// Dev-only override: only honored when build-info reports a dev build
|
|
226
|
+
// (no baked DSN). Production binaries ignore the env override entirely.
|
|
227
|
+
// OSS-facing MEETLESS_SENTRY_DSN wins; legacy MLA_SENTRY_DSN still accepted.
|
|
228
|
+
const bakedDsn = buildInfo.sentryDsn;
|
|
229
|
+
const isDev = !bakedDsn;
|
|
230
|
+
const dsn = isDev
|
|
231
|
+
? process.env.MEETLESS_SENTRY_DSN || process.env.MLA_SENTRY_DSN
|
|
232
|
+
: bakedDsn;
|
|
233
|
+
if (!dsn) {
|
|
234
|
+
sentryAvailable = false;
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
Sentry.init({
|
|
238
|
+
dsn,
|
|
239
|
+
release: buildInfo.sha,
|
|
240
|
+
environment: buildInfo.dirty ? "dev" : "prod",
|
|
241
|
+
// Pure CLI: no transaction sampling, no tracing integrations.
|
|
242
|
+
// Errors and explicit captureMessage only.
|
|
243
|
+
tracesSampleRate: 0,
|
|
244
|
+
// §9 redaction invariant (Finding K / P7): scrub credentials out of every
|
|
245
|
+
// event before transport. Must exist before any token-capturing path (login
|
|
246
|
+
// / refresh) can land a token in a breadcrumb or stack var.
|
|
247
|
+
beforeSend: (event) => redactSentryEvent(event),
|
|
248
|
+
});
|
|
249
|
+
Sentry.setTags({
|
|
250
|
+
mla_version: `${buildInfo.version} (${buildInfo.sha}${buildInfo.dirty ? "-dirty" : ""})`,
|
|
251
|
+
platform: process.platform,
|
|
252
|
+
trace_source: "mla-cli",
|
|
253
|
+
});
|
|
254
|
+
sentryAvailable = true;
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
function isSentryAvailable() {
|
|
258
|
+
return sentryAvailable;
|
|
259
|
+
}
|
|
260
|
+
// Run-local workspace config snapshot. Read by capture helpers to decide
|
|
261
|
+
// whether non-bootstrap captures are allowed (cycle 4 fix 4). null until
|
|
262
|
+
// workspace-me has loaded; bootstrap captures fire under null without
|
|
263
|
+
// consulting this gate.
|
|
264
|
+
let currentWorkspaceConfig = null;
|
|
265
|
+
function setWorkspaceConfig(cfg) {
|
|
266
|
+
currentWorkspaceConfig = cfg;
|
|
267
|
+
}
|
|
268
|
+
function getWorkspaceConfig() {
|
|
269
|
+
return currentWorkspaceConfig;
|
|
270
|
+
}
|
|
271
|
+
// Tenant guardrail (§9): even if workspace.tracing.sentryEnabled were flipped
|
|
272
|
+
// true on a non-dogfood workspace by a config drift, the CLI still refuses to
|
|
273
|
+
// send full-context captures. Only ws_an_local or workspaces explicitly flagged
|
|
274
|
+
// tracing_dogfood: true clear the gate.
|
|
275
|
+
function workspaceSentryAllowed(cfg) {
|
|
276
|
+
if (!cfg)
|
|
277
|
+
return false;
|
|
278
|
+
if (cfg.tracing?.sentryEnabled !== true)
|
|
279
|
+
return false;
|
|
280
|
+
if (cfg.workspaceId === "ws_an_local")
|
|
281
|
+
return true;
|
|
282
|
+
return cfg.tracingDogfood === true;
|
|
283
|
+
}
|
|
284
|
+
const SAFE_OBSERVABILITY_KEYS = [
|
|
285
|
+
"traceId",
|
|
286
|
+
"langfuseUrl",
|
|
287
|
+
"release",
|
|
288
|
+
"command",
|
|
289
|
+
"exitCode",
|
|
290
|
+
"traceSource",
|
|
291
|
+
"workspaceIdOrHash",
|
|
292
|
+
];
|
|
293
|
+
// Pick only the allowlisted keys from arbitrary input, dropping anything else
|
|
294
|
+
// plus null/undefined and non-scalar values (so an object smuggled into an
|
|
295
|
+
// allowlisted key still cannot leak). Exported so the OBS-9 guarantee is
|
|
296
|
+
// unit-testable without a live Sentry scope.
|
|
297
|
+
function pickSafeObservabilityFields(input) {
|
|
298
|
+
const out = {};
|
|
299
|
+
for (const key of SAFE_OBSERVABILITY_KEYS) {
|
|
300
|
+
const value = input[key];
|
|
301
|
+
if (value === undefined || value === null)
|
|
302
|
+
continue;
|
|
303
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
304
|
+
out[key] = value;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
// Attach the allowlisted observability fields as a single Sentry "observability"
|
|
310
|
+
// context block; returns the safe object actually attached (for tests). Pass the
|
|
311
|
+
// full candidate object: non-allowlisted keys never reach Sentry. When nothing
|
|
312
|
+
// safe survives, the context is set to null so no empty frame is advertised.
|
|
313
|
+
function setSafeObservabilityContext(scope, fields) {
|
|
314
|
+
const safe = pickSafeObservabilityFields(fields);
|
|
315
|
+
scope.setContext("observability", Object.keys(safe).length > 0 ? safe : null);
|
|
316
|
+
return safe;
|
|
317
|
+
}
|
|
318
|
+
// Build the OBS-9-safe observability fields for a Sentry capture from run-local
|
|
319
|
+
// state. langfuseUrl is included only when the workspace config carries a project
|
|
320
|
+
// id; otherwise it is omitted (the allowlist drops the undefined). release is the
|
|
321
|
+
// build sha so a Sentry event pins the exact binary.
|
|
322
|
+
function captureObservabilityFields(ctx) {
|
|
323
|
+
const cfg = currentWorkspaceConfig;
|
|
324
|
+
const projectId = cfg?.tracing?.langfuseProjectId ?? null;
|
|
325
|
+
return {
|
|
326
|
+
traceId: ctx.traceId,
|
|
327
|
+
langfuseUrl: projectId ? langfuseTraceUrl(projectId, ctx.traceId) : undefined,
|
|
328
|
+
release: cachedBuildInfo?.sha,
|
|
329
|
+
command: ctx.sub ? `${ctx.command} ${ctx.sub}` : ctx.command,
|
|
330
|
+
exitCode: ctx.exitCode,
|
|
331
|
+
traceSource: "mla-cli",
|
|
332
|
+
workspaceIdOrHash: cfg?.workspaceId,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function captureCliError(err, ctx) {
|
|
336
|
+
if (!sentryAvailable)
|
|
337
|
+
return;
|
|
338
|
+
if (!workspaceSentryAllowed(currentWorkspaceConfig))
|
|
339
|
+
return;
|
|
340
|
+
Sentry.withScope((scope) => {
|
|
341
|
+
scope.setTag("trace_id", ctx.traceId);
|
|
342
|
+
scope.setTag("command", ctx.command);
|
|
343
|
+
scope.setTag("sub", ctx.sub ?? "none");
|
|
344
|
+
setSafeObservabilityContext(scope, captureObservabilityFields(ctx));
|
|
345
|
+
scope.setLevel("error");
|
|
346
|
+
Sentry.captureException(err);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
function captureCliNonZeroExit(ctx) {
|
|
350
|
+
if (!sentryAvailable)
|
|
351
|
+
return;
|
|
352
|
+
if (!workspaceSentryAllowed(currentWorkspaceConfig))
|
|
353
|
+
return;
|
|
354
|
+
Sentry.withScope((scope) => {
|
|
355
|
+
scope.setTag("trace_id", ctx.traceId);
|
|
356
|
+
scope.setTag("command", ctx.command);
|
|
357
|
+
scope.setTag("sub", ctx.sub ?? "none");
|
|
358
|
+
scope.setTag("exit_code", String(ctx.exitCode));
|
|
359
|
+
setSafeObservabilityContext(scope, captureObservabilityFields(ctx));
|
|
360
|
+
scope.setLevel("warning");
|
|
361
|
+
Sentry.captureMessage(`mla ${ctx.command} exited ${ctx.exitCode}`);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
// Bootstrap captures bypass the workspace gate by design: workspace config has
|
|
365
|
+
// not loaded yet, but we still need visibility on bad token / control
|
|
366
|
+
// unreachable. Tags are minimal so PII risk is zero.
|
|
367
|
+
function captureBootstrapError(err, ctx) {
|
|
368
|
+
if (!sentryAvailable)
|
|
369
|
+
return;
|
|
370
|
+
Sentry.withScope((scope) => {
|
|
371
|
+
scope.setTag("trace_id", ctx.traceId);
|
|
372
|
+
scope.setTag("phase", "bootstrap");
|
|
373
|
+
scope.setLevel("error");
|
|
374
|
+
Sentry.captureException(err);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
|
|
378
|
+
async function boundedSentryFlush() {
|
|
379
|
+
if (!sentryAvailable)
|
|
380
|
+
return;
|
|
381
|
+
try {
|
|
382
|
+
await Promise.race([Sentry.flush(500), sleep(500)]);
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
// Never block exit on Sentry flush failure.
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Per-run trace_id storage. Read by mlaFetch to stamp X-Trace-ID on every
|
|
389
|
+
// outbound HTTP request. Set exactly once at the top of the run.
|
|
390
|
+
let currentTraceId = null;
|
|
391
|
+
function setRunTraceId(traceId) {
|
|
392
|
+
currentTraceId = traceId;
|
|
393
|
+
}
|
|
394
|
+
function getRunTraceId() {
|
|
395
|
+
return currentTraceId;
|
|
396
|
+
}
|
|
397
|
+
// Per-run agent session id storage (the raw canonical Claude UUID, NOT the
|
|
398
|
+
// composed namespaced value). Read by buildIntelHeaders to stamp
|
|
399
|
+
// X-Agent-Session-ID on intel calls so the workspace-authoritative sink composes
|
|
400
|
+
// the session exactly once (INV-COMPOSE-ONCE: the raw UUID rides the wire; intel
|
|
401
|
+
// composes). Set exactly once at the top of the run from CLAUDE_CODE_SESSION_ID,
|
|
402
|
+
// already canonicalized; null when the CLI is not running inside a Claude session
|
|
403
|
+
// or the env value is malformed (the header is then simply not stamped).
|
|
404
|
+
let currentSessionId = null;
|
|
405
|
+
function setRunSessionId(sessionId) {
|
|
406
|
+
currentSessionId = sessionId;
|
|
407
|
+
}
|
|
408
|
+
function getRunSessionId() {
|
|
409
|
+
return currentSessionId;
|
|
410
|
+
}
|
|
411
|
+
// Test-only: clear the run-local session id so suites don't bleed state.
|
|
412
|
+
function resetRunSessionIdForTesting() {
|
|
413
|
+
currentSessionId = null;
|
|
414
|
+
}
|
|
415
|
+
// Per-run analytics run_id (INV-RUN-1). Distinct identity from trace_id: minted
|
|
416
|
+
// independently, never derived from it. trace_id is the cross-system
|
|
417
|
+
// observability key; run_id is the analytics invocation key. They are 1:1 at the
|
|
418
|
+
// CLI in v1, but kept separate so hooks/MCP/child-traces can mint their own
|
|
419
|
+
// run_id under a shared trace later. uuid (not the 32-hex trace shape) so the two
|
|
420
|
+
// can never be confused.
|
|
421
|
+
let currentRunId = null;
|
|
422
|
+
function mintRunId() {
|
|
423
|
+
return crypto.randomUUID();
|
|
424
|
+
}
|
|
425
|
+
function setRunId(runId) {
|
|
426
|
+
currentRunId = runId;
|
|
427
|
+
}
|
|
428
|
+
function getRunId() {
|
|
429
|
+
return currentRunId;
|
|
430
|
+
}
|
|
431
|
+
// Test-only: clear the run-local id so suites don't bleed state across cases.
|
|
432
|
+
function resetRunIdForTesting() {
|
|
433
|
+
currentRunId = null;
|
|
434
|
+
}
|
|
435
|
+
// Per-run repo fingerprint (analytics attribution, spec section 3.7 / T1.10). A
|
|
436
|
+
// NON-identifying one-way hash of the git remote/repo the run executed in,
|
|
437
|
+
// computed ONCE at bootstrap (the git I/O lives in computeRepoFingerprint) and
|
|
438
|
+
// read back by the analytics recorder. Stored as a run-local singleton, exactly
|
|
439
|
+
// like run_id/trace_id, so buildEvent stays a pure read and never shells out to
|
|
440
|
+
// git per event. null when the run is not in a git repo (or git is unavailable);
|
|
441
|
+
// attribution then carries a null repoFingerprint rather than a fabricated one.
|
|
442
|
+
let currentRepoFingerprint = null;
|
|
443
|
+
function setRepoFingerprint(fingerprint) {
|
|
444
|
+
currentRepoFingerprint = fingerprint;
|
|
445
|
+
}
|
|
446
|
+
function getRepoFingerprint() {
|
|
447
|
+
return currentRepoFingerprint;
|
|
448
|
+
}
|
|
449
|
+
// Test-only: clear the run-local fingerprint so suites don't bleed state.
|
|
450
|
+
function resetRepoFingerprintForTesting() {
|
|
451
|
+
currentRepoFingerprint = null;
|
|
452
|
+
}
|
|
453
|
+
// Intel-echo observation (P2.4 deep-link gate). Set true when ANY intel
|
|
454
|
+
// response in this run carried our X-Trace-ID back in its response headers.
|
|
455
|
+
// Observation only: the CLI never adopts the response id (immutability is
|
|
456
|
+
// preserved by mlaFetch / intelGet / intelPost; they never read it back into
|
|
457
|
+
// currentTraceId). The deep link is printed only when tracer.flush() succeeded
|
|
458
|
+
// OR this flag is true (server-side intel route already produced a Langfuse
|
|
459
|
+
// trace under the inbound id).
|
|
460
|
+
let intelEchoedRunTraceId = false;
|
|
461
|
+
// Thrown only in strict/debug mode (see traceStrictMode) when intel positively
|
|
462
|
+
// echoes a trace id that is NOT ours. Typed so a caller can distinguish a
|
|
463
|
+
// propagation-integrity failure from an ordinary HTTP error.
|
|
464
|
+
class TraceRoundTripError extends Error {
|
|
465
|
+
sent;
|
|
466
|
+
echoed;
|
|
467
|
+
constructor(sent, echoed) {
|
|
468
|
+
super(`trace-id round-trip mismatch: sent ${sent}, intel echoed ${echoed}`);
|
|
469
|
+
this.sent = sent;
|
|
470
|
+
this.echoed = echoed;
|
|
471
|
+
this.name = "TraceRoundTripError";
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
exports.TraceRoundTripError = TraceRoundTripError;
|
|
475
|
+
// Strict trace mode (the "strict/debug flag" of spec gap 6.5 / Phase 4). When
|
|
476
|
+
// MEETLESS_TRACE_STRICT or MLA_TRACE_STRICT is truthy, a round-trip MISMATCH is
|
|
477
|
+
// fatal (throws) instead of a one-line warning. Off by default: a mismatch is a
|
|
478
|
+
// confidence-check failure, not a reason to fail a user's command. CI and local
|
|
479
|
+
// debugging flip it on to make a silent propagation break loud.
|
|
480
|
+
function traceStrictMode() {
|
|
481
|
+
const v = process.env.MEETLESS_TRACE_STRICT || process.env.MLA_TRACE_STRICT;
|
|
482
|
+
if (!v)
|
|
483
|
+
return false;
|
|
484
|
+
const norm = v.trim().toLowerCase();
|
|
485
|
+
return norm === "1" || norm === "true" || norm === "yes" || norm === "on";
|
|
486
|
+
}
|
|
487
|
+
// P4-T2: assert the trace_id round-trip against intel's echoed X-Trace-ID.
|
|
488
|
+
// - absent header -> graceful no-op (older intel, or a proxy stripped it): the
|
|
489
|
+
// deep-link gate already falls back to didTraceFlushSucceed(), so a missing
|
|
490
|
+
// echo only weakens a confidence check; it never breaks a join.
|
|
491
|
+
// - match -> record the positive confirmation.
|
|
492
|
+
// - mismatch -> a real propagation break (a proxy rewrote the header, or
|
|
493
|
+
// intel minted a fresh id because ours never arrived). Warn by default; throw
|
|
494
|
+
// a TraceRoundTripError under strict/debug. Both ids are observability join
|
|
495
|
+
// keys, not payloads, so naming them is OBS-9-safe and is exactly what you
|
|
496
|
+
// need to debug the split.
|
|
497
|
+
function noteIntelEchoedTraceId(echoedId) {
|
|
498
|
+
if (!echoedId)
|
|
499
|
+
return;
|
|
500
|
+
if (!currentTraceId)
|
|
501
|
+
return;
|
|
502
|
+
const normalized = echoedId.toLowerCase();
|
|
503
|
+
if (normalized === currentTraceId) {
|
|
504
|
+
intelEchoedRunTraceId = true;
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (traceStrictMode()) {
|
|
508
|
+
throw new TraceRoundTripError(currentTraceId, normalized);
|
|
509
|
+
}
|
|
510
|
+
process.stderr.write(`warn: trace-id round-trip mismatch (sent ${currentTraceId}, ` +
|
|
511
|
+
`intel echoed ${normalized}); traces for this run may be split\n`);
|
|
512
|
+
}
|
|
513
|
+
function didIntelEchoTraceId() {
|
|
514
|
+
return intelEchoedRunTraceId;
|
|
515
|
+
}
|
|
516
|
+
function resetIntelEchoForTesting() {
|
|
517
|
+
intelEchoedRunTraceId = false;
|
|
518
|
+
}
|
|
519
|
+
// Trace-flush outcome (P2.4 deep-link gate). True when boundedTraceFlush
|
|
520
|
+
// observed tracer.flush() resolve without throwing. The deep link is printed
|
|
521
|
+
// only when this is true OR intel echoed the run trace id; otherwise we'd be
|
|
522
|
+
// advertising URLs that resolve to nothing.
|
|
523
|
+
let traceFlushSucceeded = false;
|
|
524
|
+
function didTraceFlushSucceed() {
|
|
525
|
+
return traceFlushSucceeded;
|
|
526
|
+
}
|
|
527
|
+
function resetTraceFlushOutcomeForTesting() {
|
|
528
|
+
traceFlushSucceeded = false;
|
|
529
|
+
}
|
|
530
|
+
// HTTP-backed flush function the tracer calls inside flush(). POSTs the span
|
|
531
|
+
// batch to control's /internal/v1/agent-traces/ingest. Auth uses the same
|
|
532
|
+
// bearer the CLI uses for every other control hop (controlToken). workspaceId
|
|
533
|
+
// is injected from the CLI's run-local workspace config so control can apply
|
|
534
|
+
// the §9 tenant guardrail (only ws_an_local or tracing_dogfood workspaces are
|
|
535
|
+
// allowed to relay). On non-2xx, throw so boundedTraceFlush prints its single
|
|
536
|
+
// stderr line.
|
|
537
|
+
// The HTTP flush's per-request deadline: the AbortController aborts the fetch at
|
|
538
|
+
// this point. This is the AUTHORITATIVE trace-upload timeout.
|
|
539
|
+
exports.HTTP_FLUSH_TIMEOUT_MS = 1500;
|
|
540
|
+
// The outer ceiling boundedTraceFlush races tracer.flush() against. It MUST stay
|
|
541
|
+
// >= HTTP_FLUSH_TIMEOUT_MS so the HTTP deadline is the real timeout and this is
|
|
542
|
+
// only a backstop for a flush that hangs WITHOUT honoring its own deadline (e.g.
|
|
543
|
+
// a non-HTTP flushFn). When this was 500ms and the HTTP timeout was 1500ms, a
|
|
544
|
+
// slow-but-successful upload (500-1500ms) was killed by this race, dropped, and
|
|
545
|
+
// reported as a false "timeout" (Finding #2). The +500ms headroom lets the inner
|
|
546
|
+
// AbortController fire first on a genuinely slow network so the user sees the
|
|
547
|
+
// precise HTTP error, not this generic ceiling.
|
|
548
|
+
exports.TRACE_FLUSH_CEILING_MS = exports.HTTP_FLUSH_TIMEOUT_MS + 500;
|
|
549
|
+
function makeHttpFlush(opts) {
|
|
550
|
+
const timeout = opts.timeoutMs ?? exports.HTTP_FLUSH_TIMEOUT_MS;
|
|
551
|
+
return async (payload) => {
|
|
552
|
+
const url = `${opts.controlUrl}/internal/v1/agent-traces/ingest`;
|
|
553
|
+
const controller = new AbortController();
|
|
554
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
555
|
+
try {
|
|
556
|
+
// agent-traces ingest is a workspace-bound write behind control's
|
|
557
|
+
// AgentReviewWorkspaceGuard (INV-AUTH-1): it 403s "Actor identity
|
|
558
|
+
// required for this write" unless the caller presents an actor. lib/http.ts
|
|
559
|
+
// stamps X-Meetless-Actor on every other control hop; this hand-rolled
|
|
560
|
+
// flush must mirror that, or every self-trace upload 403s. A blank/
|
|
561
|
+
// whitespace-only actor is treated as absent (same as buildRequestHeaders).
|
|
562
|
+
const headers = {
|
|
563
|
+
Authorization: `Bearer ${opts.controlToken}`,
|
|
564
|
+
"Content-Type": "application/json",
|
|
565
|
+
"X-Trace-ID": payload.traceId,
|
|
566
|
+
};
|
|
567
|
+
if (opts.actorUserId && opts.actorUserId.trim().length > 0) {
|
|
568
|
+
headers["X-Meetless-Actor"] = opts.actorUserId;
|
|
569
|
+
}
|
|
570
|
+
const res = await fetch(url, {
|
|
571
|
+
method: "POST",
|
|
572
|
+
headers,
|
|
573
|
+
body: JSON.stringify({ ...payload, workspaceId: opts.workspaceId }),
|
|
574
|
+
signal: controller.signal,
|
|
575
|
+
});
|
|
576
|
+
if (!res.ok) {
|
|
577
|
+
const body = await res.text().catch(() => "");
|
|
578
|
+
const e = new Error(`POST ${url} -> HTTP ${res.status}: ${body.slice(0, 200)}`);
|
|
579
|
+
e.status = res.status;
|
|
580
|
+
// §9 tenant guardrail: a 403 TRACING_NOT_ENABLED_FOR_WORKSPACE is a
|
|
581
|
+
// deliberate policy refusal (this workspace simply does not relay CLI
|
|
582
|
+
// self-traces), not a failure. Tag it so boundedTraceFlush skips the
|
|
583
|
+
// user-facing warning. Any other non-2xx (auth 403, 5xx, etc.) stays
|
|
584
|
+
// a real, warnable failure.
|
|
585
|
+
if (res.status === 403 && body.includes(TRACING_DISABLED_CODE)) {
|
|
586
|
+
e.tracingDisabledByPolicy = true;
|
|
587
|
+
}
|
|
588
|
+
throw e;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
finally {
|
|
592
|
+
clearTimeout(timer);
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
// Control's §9 error code for "this workspace is not authorized to relay
|
|
597
|
+
// traces" (apps/control api-exception.ts TRACING_NOT_ENABLED_FOR_WORKSPACE).
|
|
598
|
+
// Shared by the tag site (makeHttpFlush) and the classifier so the two cannot
|
|
599
|
+
// drift on the literal.
|
|
600
|
+
const TRACING_DISABLED_CODE = "TRACING_NOT_ENABLED_FOR_WORKSPACE";
|
|
601
|
+
// True when a flush error is the §9 tracing-policy refusal: either tagged by
|
|
602
|
+
// makeHttpFlush, or (defense in depth, if the tag is ever lost in transit) a
|
|
603
|
+
// 403 whose message carries the policy code. Such a refusal is expected and
|
|
604
|
+
// must NOT surface a "trace upload failed" warning.
|
|
605
|
+
function isTracingPolicyDisabled(err) {
|
|
606
|
+
if (!err || typeof err !== "object")
|
|
607
|
+
return false;
|
|
608
|
+
const e = err;
|
|
609
|
+
if (e.tracingDisabledByPolicy === true)
|
|
610
|
+
return true;
|
|
611
|
+
return (e.status === 403 &&
|
|
612
|
+
typeof e.message === "string" &&
|
|
613
|
+
e.message.includes(TRACING_DISABLED_CODE));
|
|
614
|
+
}
|
|
615
|
+
function describeFlushErr(err) {
|
|
616
|
+
if (err && typeof err === "object") {
|
|
617
|
+
const e = err;
|
|
618
|
+
if (e.status)
|
|
619
|
+
return `HTTP ${e.status}`;
|
|
620
|
+
if (e.name === "AbortError")
|
|
621
|
+
return "timeout";
|
|
622
|
+
if (e.message)
|
|
623
|
+
return e.message.slice(0, 120);
|
|
624
|
+
}
|
|
625
|
+
return String(err).slice(0, 120);
|
|
626
|
+
}
|
|
627
|
+
// Per-run tracer storage. Read by mlaFetch / intelGet / intelPost to start
|
|
628
|
+
// child spans around each outbound HTTP call. Set exactly once at the top of
|
|
629
|
+
// the run (cli.ts runCliBootstrap). Null when the CLI is invoked without a
|
|
630
|
+
// reachable config (e.g. mla init) so HTTP layers can no-op cheaply.
|
|
631
|
+
let currentTracer = null;
|
|
632
|
+
function setRunTracer(tracer) {
|
|
633
|
+
currentTracer = tracer;
|
|
634
|
+
}
|
|
635
|
+
function getRunTracer() {
|
|
636
|
+
return currentTracer;
|
|
637
|
+
}
|
|
638
|
+
function resetRunTracerForTesting() {
|
|
639
|
+
currentTracer = null;
|
|
640
|
+
}
|
|
641
|
+
// Convert an outbound URL path into the right side of an `intel.<route>` or
|
|
642
|
+
// `control.<route>` span name. Drops `/internal/v1/` / `/v1/` prefixes, strips
|
|
643
|
+
// query strings, and collapses obvious id-shaped path segments to `:id` so a
|
|
644
|
+
// fleet of `coordination-cases.cse_xxxx` spans roll up cleanly in Langfuse.
|
|
645
|
+
const ID_LIKE = /^([a-z]+_[A-Za-z0-9]{4,}|[0-9a-f]{20,}|[0-9a-fA-F-]{36})$/;
|
|
646
|
+
function routeNameFromPath(rawPath) {
|
|
647
|
+
const qIdx = rawPath.indexOf("?");
|
|
648
|
+
const noQuery = qIdx >= 0 ? rawPath.slice(0, qIdx) : rawPath;
|
|
649
|
+
let p = noQuery;
|
|
650
|
+
if (p.startsWith("/internal/v1/"))
|
|
651
|
+
p = p.slice("/internal/v1/".length);
|
|
652
|
+
else if (p.startsWith("/v1/"))
|
|
653
|
+
p = p.slice("/v1/".length);
|
|
654
|
+
else if (p.startsWith("/"))
|
|
655
|
+
p = p.slice(1);
|
|
656
|
+
const parts = p.split("/").filter((s) => s.length > 0);
|
|
657
|
+
const sanitized = parts.map((s) => {
|
|
658
|
+
let token;
|
|
659
|
+
try {
|
|
660
|
+
token = decodeURIComponent(s);
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
token = s;
|
|
664
|
+
}
|
|
665
|
+
return ID_LIKE.test(token) ? ":id" : token;
|
|
666
|
+
});
|
|
667
|
+
return sanitized.join(".") || "root";
|
|
668
|
+
}
|
|
669
|
+
// Argv redaction for the root span attribute (P2.2 + spec §10.P2 must-test 6).
|
|
670
|
+
// Reuses the shared secret redactor so a token leaked on the command line is
|
|
671
|
+
// stripped before the trace reaches Langfuse. The redactor returns the input
|
|
672
|
+
// unchanged for empty strings, otherwise a transformed string; the cast is
|
|
673
|
+
// safe because every input here is already a string.
|
|
674
|
+
function redactArgvForSpan(argv) {
|
|
675
|
+
return argv.map((arg) => (0, redactor_1.redact)(arg));
|
|
676
|
+
}
|
|
677
|
+
// Canonical Langfuse trace URL. ONE algorithm, mirrored byte-for-byte by the
|
|
678
|
+
// Python twin (intel app/core/observability_context.py: langfuse_trace_url). A
|
|
679
|
+
// Python-built URL that drifts from this shape produces a Sentry deep-link that
|
|
680
|
+
// 404s, so the two are locked together by a cross-language fixture test
|
|
681
|
+
// (test/fixtures/langfuse-url-fixtures.json; spec gap 6.3). The CLI has no
|
|
682
|
+
// self-host config, so the host is always the Langfuse Cloud default.
|
|
683
|
+
function langfuseTraceUrl(projectId, traceId) {
|
|
684
|
+
return `https://cloud.langfuse.com/project/${projectId}/traces/${traceId}`;
|
|
685
|
+
}
|
|
686
|
+
// P2.3 / spec §8 deep-link printer. Gate is the explicit boolean conjunction:
|
|
687
|
+
// workspace config loaded, tracing.enabled true, langfuseProjectId set, AND
|
|
688
|
+
// (flush succeeded OR intel echoed the inbound X-Trace-ID). Any single
|
|
689
|
+
// condition failing returns false and prints nothing; printing when no trace
|
|
690
|
+
// landed advertises dead URLs.
|
|
691
|
+
function langfuseDeepLink(projectId, traceId) {
|
|
692
|
+
return `trace: ${langfuseTraceUrl(projectId, traceId)}`;
|
|
693
|
+
}
|
|
694
|
+
function shouldPrintDeepLink(opts) {
|
|
695
|
+
if (!opts.config)
|
|
696
|
+
return false;
|
|
697
|
+
if (opts.config.tracing?.enabled !== true)
|
|
698
|
+
return false;
|
|
699
|
+
if (!opts.config.tracing.langfuseProjectId)
|
|
700
|
+
return false;
|
|
701
|
+
return opts.flushSucceeded || opts.intelEchoed;
|
|
702
|
+
}
|
|
703
|
+
function maybePrintDeepLink(opts) {
|
|
704
|
+
if (!shouldPrintDeepLink(opts))
|
|
705
|
+
return false;
|
|
706
|
+
const projectId = opts.config.tracing.langfuseProjectId;
|
|
707
|
+
process.stdout.write(`${langfuseDeepLink(projectId, opts.traceId)}\n`);
|
|
708
|
+
return true;
|
|
709
|
+
}
|
|
710
|
+
// Build the run's tracer. Returns a no-op tracer when no HTTP flush function
|
|
711
|
+
// is available (mla init / no config / fully-offline command). Otherwise
|
|
712
|
+
// returns a real tracer whose flush POSTs to control's relay. flush() is
|
|
713
|
+
// always called once (via boundedTraceFlush in cli.ts); the no-op tracer
|
|
714
|
+
// resolves immediately so the lifecycle is unconditional.
|
|
715
|
+
function createRunTracer(opts) {
|
|
716
|
+
if (!opts.flushFn) {
|
|
717
|
+
return makeNoopTracer({ traceId: opts.traceId });
|
|
718
|
+
}
|
|
719
|
+
return makeTracer({
|
|
720
|
+
traceId: opts.traceId,
|
|
721
|
+
rootName: opts.rootName,
|
|
722
|
+
client: {
|
|
723
|
+
mlaVersion: opts.buildInfo.version,
|
|
724
|
+
platform: process.platform,
|
|
725
|
+
},
|
|
726
|
+
flushFn: opts.flushFn,
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
// Visible-failure stderr line on flush error (spec §6.2). Exactly one line, no
|
|
730
|
+
// retry, no persistence. Caller's command exit code is preserved; this never
|
|
731
|
+
// throws so it cannot promote a successful command into a failed one.
|
|
732
|
+
//
|
|
733
|
+
// The ceiling uses a cleared setTimeout (not sleep().then(throw)) so the timer
|
|
734
|
+
// is released when flush wins; otherwise it would hold the event loop open until
|
|
735
|
+
// the full ceiling elapses after success (functionally masked by CLI's
|
|
736
|
+
// process.exit, but leaks open handles in jest and would block any programmatic
|
|
737
|
+
// caller). The ceiling defaults to TRACE_FLUSH_CEILING_MS, kept wider than the
|
|
738
|
+
// HTTP flush's own deadline so a slow-but-successful upload is not killed here
|
|
739
|
+
// (Finding #2); callers/tests may pass a tighter ceiling.
|
|
740
|
+
async function boundedTraceFlush(tracer, ceilingMs = exports.TRACE_FLUSH_CEILING_MS) {
|
|
741
|
+
let timer = null;
|
|
742
|
+
let flushErr = null;
|
|
743
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
744
|
+
timer = setTimeout(() => {
|
|
745
|
+
const err = new Error(`timeout: trace flush exceeded ${ceilingMs}ms`);
|
|
746
|
+
flushErr = err;
|
|
747
|
+
reject(err);
|
|
748
|
+
}, ceilingMs);
|
|
749
|
+
});
|
|
750
|
+
try {
|
|
751
|
+
let flushPromise;
|
|
752
|
+
try {
|
|
753
|
+
flushPromise = Promise.resolve(tracer.flush());
|
|
754
|
+
}
|
|
755
|
+
catch (err) {
|
|
756
|
+
// A synchronous throw inside tracer.flush() before it returns a promise.
|
|
757
|
+
flushPromise = Promise.reject(err);
|
|
758
|
+
}
|
|
759
|
+
await Promise.race([
|
|
760
|
+
flushPromise.catch((e) => {
|
|
761
|
+
flushErr = e;
|
|
762
|
+
throw e;
|
|
763
|
+
}),
|
|
764
|
+
timeoutPromise,
|
|
765
|
+
]);
|
|
766
|
+
traceFlushSucceeded = true;
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
// Not relayed either way, so no trace URL to advertise (cli.ts gates on
|
|
770
|
+
// didTraceFlushSucceed()).
|
|
771
|
+
traceFlushSucceeded = false;
|
|
772
|
+
// A §9 tracing-policy refusal is expected, not a failure: stay silent.
|
|
773
|
+
// Every real failure (auth, 5xx, connection refused, timeout) still warns.
|
|
774
|
+
if (!isTracingPolicyDisabled(flushErr)) {
|
|
775
|
+
process.stderr.write(`warn: trace upload failed (${describeFlushErr(flushErr)}); ` +
|
|
776
|
+
`Sentry event still carries trace_id\n`);
|
|
777
|
+
// F8 (telemetry-upload-failed): the trace upload itself failed on a real,
|
|
778
|
+
// non-policy error. Record it to the local deadletter so the failure is
|
|
779
|
+
// never silently lost (a telemetry system that fails silently gives false
|
|
780
|
+
// "no alerts = no problems"). Per INV-SENTRY-NOISE-BUDGET this is
|
|
781
|
+
// local-deadletter + the local warning above, NOT a Sentry event from the
|
|
782
|
+
// CLI. recordTelemetryUploadFailure respects the kill switch and never
|
|
783
|
+
// throws; the extra guard keeps a future change from breaking the flush
|
|
784
|
+
// path (this must never promote a successful command into a failed one).
|
|
785
|
+
try {
|
|
786
|
+
const wsCfg = getWorkspaceConfig();
|
|
787
|
+
const status = flushErr?.status;
|
|
788
|
+
(0, failure_telemetry_1.recordTelemetryUploadFailure)({
|
|
789
|
+
traceId: tracer.traceId,
|
|
790
|
+
workspaceId: wsCfg?.workspaceId ?? null,
|
|
791
|
+
surface: "mla-cli",
|
|
792
|
+
reasonCode: "trace_upload_failed",
|
|
793
|
+
status: typeof status === "number" ? status : undefined,
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
catch {
|
|
797
|
+
// F8 recording is best-effort; swallow so flush stays non-throwing.
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
finally {
|
|
802
|
+
if (timer)
|
|
803
|
+
clearTimeout(timer);
|
|
804
|
+
}
|
|
805
|
+
}
|