@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
package/dist/lib/http.js
ADDED
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DEFAULT_INTEL_URL = void 0;
|
|
37
|
+
exports.buildRequestHeaders = buildRequestHeaders;
|
|
38
|
+
exports.refreshUserToken = refreshUserToken;
|
|
39
|
+
exports.get = get;
|
|
40
|
+
exports.post = post;
|
|
41
|
+
exports.patch = patch;
|
|
42
|
+
exports.buildIntelHeaders = buildIntelHeaders;
|
|
43
|
+
exports.intelGet = intelGet;
|
|
44
|
+
exports.intelPost = intelPost;
|
|
45
|
+
exports.intelPatch = intelPatch;
|
|
46
|
+
exports.ping = ping;
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const config_1 = require("./config");
|
|
49
|
+
const auth_breaker_1 = require("./auth-breaker");
|
|
50
|
+
const observability_1 = require("./observability");
|
|
51
|
+
function buildError(status, body, method, url) {
|
|
52
|
+
const e = new Error(`${method} ${url} -> HTTP ${status}: ${body.slice(0, 500)}`);
|
|
53
|
+
e.status = status;
|
|
54
|
+
e.body = body;
|
|
55
|
+
return e;
|
|
56
|
+
}
|
|
57
|
+
// Wedge v6 Epoch 28: Build per-request headers. Content-Type is set ONLY when
|
|
58
|
+
// there is a body. Sending `Content-Type: application/json` on a body-less GET
|
|
59
|
+
// is HTTP-semantically wrong (RFC 7231 §3.1.1.5) AND a documented platform
|
|
60
|
+
// trap: Express's `body-parser` json() middleware on certain Node versions
|
|
61
|
+
// silently 400s a body-less request that advertises a JSON content type. The
|
|
62
|
+
// failure mode is invisible (no body in the 400 response) and the CLI's
|
|
63
|
+
// HttpError surfaces "HTTP 400: " with no diagnostic. Past production breakage
|
|
64
|
+
// is recorded in CLAUDE.md "Hard-Won Platform Lessons" -> macOS/Node.js.
|
|
65
|
+
//
|
|
66
|
+
// T1.4 (folder = workspace): when an actor is supplied (cli-config.actorUserId)
|
|
67
|
+
// it is stamped as X-Meetless-Actor on EVERY control request. The membership
|
|
68
|
+
// guard (INV-AUTH-1) needs the caller identity to resolve a WorkspaceUser; the
|
|
69
|
+
// header is harmless on reads and load-bearing on agent-review writes (it also
|
|
70
|
+
// covers agent-traces, which never carry an actor in the body). A blank/
|
|
71
|
+
// whitespace-only actor is treated as absent so the config-less `mla init` path
|
|
72
|
+
// stays header-free.
|
|
73
|
+
function buildRequestHeaders(token, hasBody, actorUserId) {
|
|
74
|
+
const h = {
|
|
75
|
+
Authorization: `Bearer ${token}`,
|
|
76
|
+
};
|
|
77
|
+
if (hasBody) {
|
|
78
|
+
h["Content-Type"] = "application/json";
|
|
79
|
+
}
|
|
80
|
+
// Stamp the run's trace_id on every outbound request so Sentry tags, intel's
|
|
81
|
+
// Langfuse traces, and any server-side scope share a single id. CLI never
|
|
82
|
+
// reads X-Trace-ID off the response; the run's id is immutable.
|
|
83
|
+
const traceId = (0, observability_1.getRunTraceId)();
|
|
84
|
+
if (traceId) {
|
|
85
|
+
h["X-Trace-ID"] = traceId;
|
|
86
|
+
}
|
|
87
|
+
if (actorUserId && actorUserId.trim().length > 0) {
|
|
88
|
+
h["X-Meetless-Actor"] = actorUserId;
|
|
89
|
+
}
|
|
90
|
+
return h;
|
|
91
|
+
}
|
|
92
|
+
// P2.1 / P2.2: span helper. Child spans wrap every outbound HTTP call so the
|
|
93
|
+
// Langfuse trace renders one span per `mlaFetch` with route, http.status, and
|
|
94
|
+
// latency_ms attributes. plane is "intel" or "control"; route is derived from
|
|
95
|
+
// the URL path via routeNameFromPath so id-shaped segments roll up cleanly.
|
|
96
|
+
// Returns null when no tracer is registered (mla init / config-less paths),
|
|
97
|
+
// so the http layer no-ops cheaply.
|
|
98
|
+
function startHttpSpan(plane, method, path) {
|
|
99
|
+
const tracer = (0, observability_1.getRunTracer)();
|
|
100
|
+
if (!tracer)
|
|
101
|
+
return { handle: null, startMs: Date.now() };
|
|
102
|
+
const handle = tracer.startSpan({
|
|
103
|
+
name: `${plane}.${(0, observability_1.routeNameFromPath)(path)}`,
|
|
104
|
+
});
|
|
105
|
+
handle.setAttribute("http.method", method);
|
|
106
|
+
handle.setAttribute("route", path);
|
|
107
|
+
return { handle, startMs: Date.now() };
|
|
108
|
+
}
|
|
109
|
+
function endHttpSpan(ctx, outcome) {
|
|
110
|
+
const { handle, startMs } = ctx;
|
|
111
|
+
if (!handle)
|
|
112
|
+
return;
|
|
113
|
+
const latencyMs = Date.now() - startMs;
|
|
114
|
+
handle.setAttribute("latency_ms", latencyMs);
|
|
115
|
+
if (outcome.kind === "ok") {
|
|
116
|
+
handle.setAttribute("http.status", outcome.status);
|
|
117
|
+
handle.end({ status: "ok", output: { status: outcome.status, latency_ms: latencyMs } });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (outcome.kind === "http_error") {
|
|
121
|
+
handle.setAttribute("http.status", outcome.status);
|
|
122
|
+
handle.end({
|
|
123
|
+
status: "error",
|
|
124
|
+
output: { status: outcome.status, latency_ms: latencyMs },
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
handle.end({ status: "error", error: outcome.error, output: { latency_ms: latencyMs } });
|
|
129
|
+
}
|
|
130
|
+
// Single-shot control request: exactly one fetch, no auth-mode policy, no retry.
|
|
131
|
+
// doFetch (below) wraps this with the none-mode fail-fast and the user-token
|
|
132
|
+
// auto-refresh dance (§6.5). Splitting them keeps the refresh retry a clean
|
|
133
|
+
// "call doFetchOnce again with the rotated token" rather than re-entrant.
|
|
134
|
+
async function doFetchOnce(cfg, method, path, body, timeoutMs = 10000) {
|
|
135
|
+
const url = `${cfg.controlUrl}${path}`;
|
|
136
|
+
const controller = new AbortController();
|
|
137
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
138
|
+
const hasBody = body !== undefined && body !== null;
|
|
139
|
+
const span = startHttpSpan("control", method, path);
|
|
140
|
+
try {
|
|
141
|
+
let res;
|
|
142
|
+
try {
|
|
143
|
+
res = await fetch(url, {
|
|
144
|
+
method,
|
|
145
|
+
headers: buildRequestHeaders(cfg.controlToken, hasBody, cfg.actorUserId),
|
|
146
|
+
body: hasBody ? JSON.stringify(body) : undefined,
|
|
147
|
+
signal: controller.signal,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
endHttpSpan(span, { kind: "network_error", error: err });
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
154
|
+
const text = await res.text();
|
|
155
|
+
if (!res.ok) {
|
|
156
|
+
endHttpSpan(span, { kind: "http_error", status: res.status });
|
|
157
|
+
throw buildError(res.status, text, method, url);
|
|
158
|
+
}
|
|
159
|
+
endHttpSpan(span, { kind: "ok", status: res.status });
|
|
160
|
+
if (!text)
|
|
161
|
+
return {};
|
|
162
|
+
try {
|
|
163
|
+
return JSON.parse(text);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return { raw: text };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
clearTimeout(timer);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Control request with the §6.4/§6.5 auth policy layered on top of doFetchOnce:
|
|
174
|
+
// - `mode: 'none'` -> fail fast with "not logged in" (Blocking 3), unless the
|
|
175
|
+
// caller is an unauthenticated probe (doctor health).
|
|
176
|
+
// - `mode: 'user-token'` + 401 -> transparently refresh the access token once
|
|
177
|
+
// (concurrency-safe, §6.5 clause 7) and retry the original request ONCE. A
|
|
178
|
+
// second 401 becomes `auth_expired`.
|
|
179
|
+
// - `mode: 'shared-key'` -> a 401 propagates directly (the operator rotated the
|
|
180
|
+
// shared key out of band; they must re-run `mla init --control-token <NEW>`).
|
|
181
|
+
async function doFetch(cfg, method, path, body, timeoutMs = 10000, opts = {}) {
|
|
182
|
+
if (!opts.allowUnauthenticated && cfg.auth.mode === "none") {
|
|
183
|
+
throw notLoggedInError();
|
|
184
|
+
}
|
|
185
|
+
// Dead-auth circuit breaker. Once control has REJECTED this exact on-disk
|
|
186
|
+
// refresh token (tripAuthBreaker, below), fail fast WITHOUT touching control so
|
|
187
|
+
// a dead session's hooks (heartbeat, steer-sync, flush) stop self-DoSing it
|
|
188
|
+
// with a validate+refresh storm. consultAuthBreaker re-reads disk and self-
|
|
189
|
+
// clears the moment the token changes (an `mla login`), so a re-login heals even
|
|
190
|
+
// the long-lived `mla mcp` workers live. Skipped for unauthenticated probes and
|
|
191
|
+
// for shared-key (no refresh token to be rejected).
|
|
192
|
+
if (!opts.allowUnauthenticated &&
|
|
193
|
+
cfg.auth.mode === "user-token" &&
|
|
194
|
+
(0, auth_breaker_1.consultAuthBreaker)()) {
|
|
195
|
+
throw authExpiredError();
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
return await doFetchOnce(cfg, method, path, body, timeoutMs);
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
const err = e;
|
|
202
|
+
// Auto-refresh applies ONLY to a user-token 401. Unauthenticated probes,
|
|
203
|
+
// shared-key, none, network errors, and non-401 statuses all propagate.
|
|
204
|
+
if (opts.allowUnauthenticated ||
|
|
205
|
+
err.status !== 401 ||
|
|
206
|
+
cfg.auth.mode !== "user-token") {
|
|
207
|
+
throw e;
|
|
208
|
+
}
|
|
209
|
+
const outcome = await refreshUserToken(cfg);
|
|
210
|
+
if (outcome === "busy") {
|
|
211
|
+
throw refreshBusyError();
|
|
212
|
+
}
|
|
213
|
+
if (outcome === "expired") {
|
|
214
|
+
throw authExpiredError();
|
|
215
|
+
}
|
|
216
|
+
// "refreshed": cfg now carries the rotated token. Retry exactly once.
|
|
217
|
+
try {
|
|
218
|
+
const out = await doFetchOnce(cfg, method, path, body, timeoutMs);
|
|
219
|
+
// A call that succeeds after a refresh proves auth recovered; ensure no
|
|
220
|
+
// dead-auth sentinel lingers (belt-and-suspenders with consult's self-clear).
|
|
221
|
+
(0, auth_breaker_1.clearAuthBreaker)();
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
catch (e2) {
|
|
225
|
+
const err2 = e2;
|
|
226
|
+
if (err2.status === 401) {
|
|
227
|
+
throw authExpiredError();
|
|
228
|
+
}
|
|
229
|
+
throw e2;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Auth-policy errors (§6.4, §6.5). All carry an empty `body` and a
|
|
235
|
+
// human-readable message that NEVER contains a token.
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
function notLoggedInError() {
|
|
238
|
+
const e = new Error("Not logged in. Run `mla login` (or `mla init --control-token <T>`).");
|
|
239
|
+
e.body = "";
|
|
240
|
+
return e;
|
|
241
|
+
}
|
|
242
|
+
function authExpiredError() {
|
|
243
|
+
// §6.5: invisible until the refresh token itself expires (~30 days idle).
|
|
244
|
+
const e = new Error("Your CLI login expired. Run `mla login`.");
|
|
245
|
+
e.status = 401;
|
|
246
|
+
e.body = "";
|
|
247
|
+
return e;
|
|
248
|
+
}
|
|
249
|
+
function refreshBusyError() {
|
|
250
|
+
const e = new Error("Another mla process is refreshing the login. Retry in a moment.");
|
|
251
|
+
e.body = "";
|
|
252
|
+
return e;
|
|
253
|
+
}
|
|
254
|
+
// Fail-fast guard for the intel plane (which always needs a real bearer; intel
|
|
255
|
+
// validates it via control, §7). Mirrors doFetch's none-mode reject. Intel does
|
|
256
|
+
// NOT auto-refresh in v1: refresh is scoped to control's doFetch (§6.5). A
|
|
257
|
+
// user-token whose access token expired refreshes on its next control call; the
|
|
258
|
+
// rotated token (cfg mutated in place) is then used by any later intel call in
|
|
259
|
+
// the same run.
|
|
260
|
+
function assertIntelAuthed(cfg) {
|
|
261
|
+
if (cfg.auth.mode === "none") {
|
|
262
|
+
throw notLoggedInError();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// Concurrency-safe access-token refresh (§6.5, §0.01 clause 7).
|
|
267
|
+
//
|
|
268
|
+
// Two `mla` processes (e.g. the detached auto-index loop and an interactive
|
|
269
|
+
// `mla review`) can 401 on the SAME on-disk refresh token at the same instant.
|
|
270
|
+
// Refresh tokens are single-use (the server rotates on every call, §9), so a
|
|
271
|
+
// naive double-refresh would let one rotation win and tear the other's session
|
|
272
|
+
// down with a spurious "login expired". The lock + re-read makes the loser adopt
|
|
273
|
+
// the winner's freshly-rotated token instead of POSTing a now-dead one.
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// Sidecar advisory lock, NOT the config file itself: a crashed holder can never
|
|
276
|
+
// corrupt cli-config.json, and a stale lock is safe to steal.
|
|
277
|
+
const LOCK_PATH = `${config_1.CFG_PATH}.lock`;
|
|
278
|
+
// Cap the wait for an interactive command (§6.5 clause 1: "e.g. 5s"). On expiry
|
|
279
|
+
// we surface "retry" rather than hang.
|
|
280
|
+
const LOCK_WAIT_CAP_MS = 5000;
|
|
281
|
+
const LOCK_POLL_MS = 75;
|
|
282
|
+
// A lock older than this is treated as abandoned (holder crashed) and stolen.
|
|
283
|
+
// Comfortably above the refresh HTTP timeout so we never steal a live refresh.
|
|
284
|
+
const LOCK_STALE_MS = 30000;
|
|
285
|
+
const REFRESH_TIMEOUT_MS = 10000;
|
|
286
|
+
// Treat an access token expiring within this window as already-expired, so we
|
|
287
|
+
// never adopt a token that would die mid-request.
|
|
288
|
+
const ACCESS_SKEW_MS = 5000;
|
|
289
|
+
function sleep(ms) {
|
|
290
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
291
|
+
}
|
|
292
|
+
// Acquire the exclusive sidecar lock. Returns the open fd, or null if the cap
|
|
293
|
+
// elapsed while another process held it (caller maps null -> "busy"/"retry").
|
|
294
|
+
async function acquireRefreshLock() {
|
|
295
|
+
const deadline = Date.now() + LOCK_WAIT_CAP_MS;
|
|
296
|
+
for (;;) {
|
|
297
|
+
try {
|
|
298
|
+
// `wx`: create-and-fail-if-exists is the atomic test-and-set.
|
|
299
|
+
const fd = fs.openSync(LOCK_PATH, "wx");
|
|
300
|
+
try {
|
|
301
|
+
fs.writeSync(fd, `${process.pid} ${Date.now()}\n`);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// Best effort: the lock IS the file's existence, not its content.
|
|
305
|
+
}
|
|
306
|
+
return fd;
|
|
307
|
+
}
|
|
308
|
+
catch (e) {
|
|
309
|
+
if (e.code !== "EEXIST")
|
|
310
|
+
throw e;
|
|
311
|
+
// Held. Steal only a clearly-abandoned (stale) lock, then retry.
|
|
312
|
+
try {
|
|
313
|
+
const age = Date.now() - fs.statSync(LOCK_PATH).mtimeMs;
|
|
314
|
+
if (age > LOCK_STALE_MS) {
|
|
315
|
+
fs.unlinkSync(LOCK_PATH);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// Holder released between EEXIST and stat: just retry the create.
|
|
321
|
+
}
|
|
322
|
+
if (Date.now() >= deadline)
|
|
323
|
+
return null;
|
|
324
|
+
await sleep(LOCK_POLL_MS);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function releaseRefreshLock(fd) {
|
|
329
|
+
if (fd === null)
|
|
330
|
+
return;
|
|
331
|
+
try {
|
|
332
|
+
fs.closeSync(fd);
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
/* ignore */
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
fs.unlinkSync(LOCK_PATH);
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
// A stale-steal by another process may have already removed it.
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function accessTokenStillFresh(accessExpiresAt) {
|
|
345
|
+
const ms = Date.parse(accessExpiresAt) - Date.now();
|
|
346
|
+
return !Number.isNaN(ms) && ms > ACCESS_SKEW_MS;
|
|
347
|
+
}
|
|
348
|
+
// Adopt a (re-read or freshly-rotated) user-token into the caller's in-memory
|
|
349
|
+
// cfg so the retry, and the rest of this run, use it without re-reading disk.
|
|
350
|
+
function adoptAuth(cfg, auth) {
|
|
351
|
+
cfg.auth = auth;
|
|
352
|
+
cfg.controlToken = auth.accessToken;
|
|
353
|
+
cfg.actorUserId = auth.user.id;
|
|
354
|
+
}
|
|
355
|
+
// POST the refresh token (body proof-of-possession; NO Authorization header,
|
|
356
|
+
// the access token is dead). The refresh token is NEVER logged. Returns the
|
|
357
|
+
// wire body, or a sentinel: "unauthorized" (refresh token itself is dead ->
|
|
358
|
+
// re-login) vs "transient" (network/5xx -> retry, session untouched).
|
|
359
|
+
async function callRefresh(controlUrl, refreshToken) {
|
|
360
|
+
const url = `${controlUrl.replace(/\/+$/, "")}/internal/v1/auth/token/refresh`;
|
|
361
|
+
const controller = new AbortController();
|
|
362
|
+
const timer = setTimeout(() => controller.abort(), REFRESH_TIMEOUT_MS);
|
|
363
|
+
try {
|
|
364
|
+
let res;
|
|
365
|
+
try {
|
|
366
|
+
res = await fetch(url, {
|
|
367
|
+
method: "POST",
|
|
368
|
+
headers: { "Content-Type": "application/json" },
|
|
369
|
+
body: JSON.stringify({ refreshToken }),
|
|
370
|
+
signal: controller.signal,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return "transient"; // timeout / DNS / connection refused
|
|
375
|
+
}
|
|
376
|
+
if (res.status === 401 || res.status === 410)
|
|
377
|
+
return "unauthorized";
|
|
378
|
+
if (!res.ok)
|
|
379
|
+
return "transient"; // 5xx etc: server broken, not session dead
|
|
380
|
+
const text = await res.text();
|
|
381
|
+
try {
|
|
382
|
+
return JSON.parse(text);
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
return "transient";
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
finally {
|
|
389
|
+
clearTimeout(timer);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// The lock + re-read + (maybe) rotate critical section (§6.5 clauses 1-3). The
|
|
393
|
+
// lock is held ONLY here, never across the original API request. Releases on
|
|
394
|
+
// every exit path via finally.
|
|
395
|
+
//
|
|
396
|
+
// Exported (Part 3) so `mla _internal refresh` can trigger the SAME
|
|
397
|
+
// concurrency-safe refresh the in-process auto-refresh uses. The hook-triggered
|
|
398
|
+
// caller is byte-identical to the doFetch caller from the config file's view: it
|
|
399
|
+
// shares the sidecar lock, single-flight re-read, and atomic writeConfig. Bash
|
|
400
|
+
// performs no token crypto, persistence, or refresh HTTP of its own.
|
|
401
|
+
async function refreshUserToken(cfg) {
|
|
402
|
+
// Defensive: doFetch only calls this for user-token, but guard anyway.
|
|
403
|
+
if (cfg.auth.mode !== "user-token")
|
|
404
|
+
return "expired";
|
|
405
|
+
const fd = await acquireRefreshLock();
|
|
406
|
+
if (fd === null)
|
|
407
|
+
return "busy"; // another process is mid-refresh; tell operator to retry
|
|
408
|
+
try {
|
|
409
|
+
// Clause 2: re-read AFTER the lock. Another process may have rotated while
|
|
410
|
+
// we waited.
|
|
411
|
+
let fresh;
|
|
412
|
+
try {
|
|
413
|
+
fresh = (0, config_1.readConfig)();
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
// Config became unreadable (corrupt, or the Gate-4 env conflict appeared
|
|
417
|
+
// mid-run). We cannot safely refresh; the operator must re-login / fix env.
|
|
418
|
+
return "expired";
|
|
419
|
+
}
|
|
420
|
+
if (fresh.auth.mode !== "user-token") {
|
|
421
|
+
// Another process logged out or downgraded the config underneath us.
|
|
422
|
+
return "expired";
|
|
423
|
+
}
|
|
424
|
+
// Already rotated by another process: adopt it, NO network call (this is the
|
|
425
|
+
// case that prevents the double-rotation race).
|
|
426
|
+
if (accessTokenStillFresh(fresh.auth.accessExpiresAt)) {
|
|
427
|
+
adoptAuth(cfg, fresh.auth);
|
|
428
|
+
return "refreshed";
|
|
429
|
+
}
|
|
430
|
+
// Clause 3: still expired -> rotate against control.
|
|
431
|
+
const rotated = await callRefresh(fresh.controlUrl, fresh.auth.refreshToken);
|
|
432
|
+
if (rotated === "transient") {
|
|
433
|
+
// Do NOT tear the session down on a transient outage: the on-disk refresh
|
|
434
|
+
// token is untouched and still valid. Surface as "retry".
|
|
435
|
+
return "busy";
|
|
436
|
+
}
|
|
437
|
+
if (rotated === "unauthorized") {
|
|
438
|
+
// The refresh token itself was REJECTED (401/410): the session is genuinely
|
|
439
|
+
// dead, not throttled. Trip the breaker keyed to THIS token so every later
|
|
440
|
+
// call (this process and the other hooks/workers sharing the config) fails
|
|
441
|
+
// fast instead of re-hammering control. A transient/throttled outcome maps to
|
|
442
|
+
// "transient"->"busy" above and never reaches here, so a rate-limit burst
|
|
443
|
+
// (the server's new 429) can never trip the breaker.
|
|
444
|
+
(0, auth_breaker_1.tripAuthBreaker)(fresh.auth.refreshToken, "refresh_rejected");
|
|
445
|
+
return "expired";
|
|
446
|
+
}
|
|
447
|
+
if (rotated.accessToken === null) {
|
|
448
|
+
// RaceRecoveryResult: the server saw a benign race and minted no new pair.
|
|
449
|
+
// Re-read once: the winning process's rotation may now be on disk. NEVER
|
|
450
|
+
// writeConfig the null tokens.
|
|
451
|
+
let after;
|
|
452
|
+
try {
|
|
453
|
+
after = (0, config_1.readConfig)();
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
return "expired";
|
|
457
|
+
}
|
|
458
|
+
if (after.auth.mode === "user-token" &&
|
|
459
|
+
accessTokenStillFresh(after.auth.accessExpiresAt)) {
|
|
460
|
+
adoptAuth(cfg, after.auth);
|
|
461
|
+
return "refreshed";
|
|
462
|
+
}
|
|
463
|
+
return "expired";
|
|
464
|
+
}
|
|
465
|
+
// Normal rotation. Refresh does not change identity; only the tokens rotate,
|
|
466
|
+
// so preserve user + sessionId (fall back to the wire sessionId if present).
|
|
467
|
+
if (!rotated.refreshToken ||
|
|
468
|
+
!rotated.accessExpiresAt ||
|
|
469
|
+
!rotated.refreshExpiresAt) {
|
|
470
|
+
// Malformed success body: treat as transient rather than persist a partial
|
|
471
|
+
// credential.
|
|
472
|
+
return "busy";
|
|
473
|
+
}
|
|
474
|
+
const newAuth = {
|
|
475
|
+
mode: "user-token",
|
|
476
|
+
accessToken: rotated.accessToken,
|
|
477
|
+
refreshToken: rotated.refreshToken,
|
|
478
|
+
accessExpiresAt: rotated.accessExpiresAt,
|
|
479
|
+
refreshExpiresAt: rotated.refreshExpiresAt,
|
|
480
|
+
sessionId: rotated.sessionId ?? fresh.auth.sessionId,
|
|
481
|
+
user: fresh.auth.user,
|
|
482
|
+
};
|
|
483
|
+
(0, config_1.writeConfig)({
|
|
484
|
+
...fresh,
|
|
485
|
+
auth: newAuth,
|
|
486
|
+
controlToken: newAuth.accessToken,
|
|
487
|
+
actorUserId: newAuth.user.id,
|
|
488
|
+
});
|
|
489
|
+
adoptAuth(cfg, newAuth);
|
|
490
|
+
return "refreshed";
|
|
491
|
+
}
|
|
492
|
+
finally {
|
|
493
|
+
releaseRefreshLock(fd);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async function get(cfg, path, timeoutMs) {
|
|
497
|
+
return (await doFetch(cfg, "GET", path, undefined, timeoutMs));
|
|
498
|
+
}
|
|
499
|
+
async function post(cfg, path, body, timeoutMs) {
|
|
500
|
+
return (await doFetch(cfg, "POST", path, body, timeoutMs));
|
|
501
|
+
}
|
|
502
|
+
async function patch(cfg, path, body, timeoutMs) {
|
|
503
|
+
return (await doFetch(cfg, "PATCH", path, body, timeoutMs));
|
|
504
|
+
}
|
|
505
|
+
// Intel reads (KB inspector, T18). Intel is a SEPARATE base URL from control
|
|
506
|
+
// (cfg.intelUrl, default 127.0.0.1:8100) but accepts the same bearer the hook
|
|
507
|
+
// uses for /v1/intercept + /v1/ask: cfg.controlToken IS intel's INTERNAL_API_KEY
|
|
508
|
+
// in the dogfood config (see user-prompt-submit.sh INTEL_TOKEN). Keeping the
|
|
509
|
+
// token source identical to the hook avoids a second secret in cli-config.json.
|
|
510
|
+
exports.DEFAULT_INTEL_URL = "http://127.0.0.1:8100";
|
|
511
|
+
// Build per-request headers for intel calls. Mirrors buildRequestHeaders for
|
|
512
|
+
// control: stamp X-Trace-ID when a run-local id exists so intel adopts it as
|
|
513
|
+
// the Langfuse trace id (intel/app/core/context.py:55). hasBody gates
|
|
514
|
+
// Content-Type to avoid the Express bodyParser silent-400 trap on GET.
|
|
515
|
+
function buildIntelHeaders(token, hasBody) {
|
|
516
|
+
const h = {
|
|
517
|
+
Authorization: `Bearer ${token}`,
|
|
518
|
+
};
|
|
519
|
+
if (hasBody) {
|
|
520
|
+
h["Content-Type"] = "application/json";
|
|
521
|
+
}
|
|
522
|
+
const traceId = (0, observability_1.getRunTraceId)();
|
|
523
|
+
if (traceId) {
|
|
524
|
+
h["X-Trace-ID"] = traceId;
|
|
525
|
+
}
|
|
526
|
+
// X-Agent-Session-ID carries the raw canonical Claude UUID (Channel A). Intel
|
|
527
|
+
// stores it verbatim on RequestContext and composes the workspace-namespaced
|
|
528
|
+
// Langfuse session exactly once at its telemetry sink, so the CLI never sends
|
|
529
|
+
// the composed value. Stamped only when a run-local session id exists; absent
|
|
530
|
+
// means "no agent session" (console fallback at intel), and the value is
|
|
531
|
+
// already canonicalized so it cannot inject a header.
|
|
532
|
+
const sessionId = (0, observability_1.getRunSessionId)();
|
|
533
|
+
if (sessionId) {
|
|
534
|
+
h["X-Agent-Session-ID"] = sessionId;
|
|
535
|
+
}
|
|
536
|
+
return h;
|
|
537
|
+
}
|
|
538
|
+
async function intelGet(cfg, path, timeoutMs = 10000) {
|
|
539
|
+
assertIntelAuthed(cfg);
|
|
540
|
+
const base = cfg.intelUrl || exports.DEFAULT_INTEL_URL;
|
|
541
|
+
const url = `${base}${path}`;
|
|
542
|
+
const controller = new AbortController();
|
|
543
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
544
|
+
const span = startHttpSpan("intel", "GET", path);
|
|
545
|
+
try {
|
|
546
|
+
let res;
|
|
547
|
+
try {
|
|
548
|
+
res = await fetch(url, {
|
|
549
|
+
method: "GET",
|
|
550
|
+
headers: buildIntelHeaders(cfg.controlToken, false),
|
|
551
|
+
signal: controller.signal,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
catch (err) {
|
|
555
|
+
endHttpSpan(span, { kind: "network_error", error: err });
|
|
556
|
+
throw err;
|
|
557
|
+
}
|
|
558
|
+
(0, observability_1.noteIntelEchoedTraceId)(res.headers.get("x-trace-id"));
|
|
559
|
+
const text = await res.text();
|
|
560
|
+
if (!res.ok) {
|
|
561
|
+
endHttpSpan(span, { kind: "http_error", status: res.status });
|
|
562
|
+
throw buildError(res.status, text, "GET", url);
|
|
563
|
+
}
|
|
564
|
+
endHttpSpan(span, { kind: "ok", status: res.status });
|
|
565
|
+
if (!text)
|
|
566
|
+
return {};
|
|
567
|
+
try {
|
|
568
|
+
return JSON.parse(text);
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
return { raw: text };
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
finally {
|
|
575
|
+
clearTimeout(timer);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
async function intelPost(cfg, path, body, timeoutMs = 15000) {
|
|
579
|
+
assertIntelAuthed(cfg);
|
|
580
|
+
const base = cfg.intelUrl || exports.DEFAULT_INTEL_URL;
|
|
581
|
+
const url = `${base}${path}`;
|
|
582
|
+
const controller = new AbortController();
|
|
583
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
584
|
+
const span = startHttpSpan("intel", "POST", path);
|
|
585
|
+
try {
|
|
586
|
+
let res;
|
|
587
|
+
try {
|
|
588
|
+
res = await fetch(url, {
|
|
589
|
+
method: "POST",
|
|
590
|
+
headers: buildIntelHeaders(cfg.controlToken, true),
|
|
591
|
+
body: JSON.stringify(body),
|
|
592
|
+
signal: controller.signal,
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
catch (err) {
|
|
596
|
+
endHttpSpan(span, { kind: "network_error", error: err });
|
|
597
|
+
throw err;
|
|
598
|
+
}
|
|
599
|
+
(0, observability_1.noteIntelEchoedTraceId)(res.headers.get("x-trace-id"));
|
|
600
|
+
const text = await res.text();
|
|
601
|
+
if (!res.ok) {
|
|
602
|
+
endHttpSpan(span, { kind: "http_error", status: res.status });
|
|
603
|
+
throw buildError(res.status, text, "POST", url);
|
|
604
|
+
}
|
|
605
|
+
endHttpSpan(span, { kind: "ok", status: res.status });
|
|
606
|
+
if (!text)
|
|
607
|
+
return {};
|
|
608
|
+
try {
|
|
609
|
+
return JSON.parse(text);
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
return { raw: text };
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
finally {
|
|
616
|
+
clearTimeout(timer);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
// Intel writes via PATCH (KB posture flip, `mla kb promote`). Mirrors intelPost
|
|
620
|
+
// exactly: same intel base URL, same buildIntelHeaders(controlToken, true), same
|
|
621
|
+
// span plane, same error handling, same JSON parse. Only the HTTP method differs.
|
|
622
|
+
async function intelPatch(cfg, path, body, timeoutMs = 15000) {
|
|
623
|
+
assertIntelAuthed(cfg);
|
|
624
|
+
const base = cfg.intelUrl || exports.DEFAULT_INTEL_URL;
|
|
625
|
+
const url = `${base}${path}`;
|
|
626
|
+
const controller = new AbortController();
|
|
627
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
628
|
+
const span = startHttpSpan("intel", "PATCH", path);
|
|
629
|
+
try {
|
|
630
|
+
let res;
|
|
631
|
+
try {
|
|
632
|
+
res = await fetch(url, {
|
|
633
|
+
method: "PATCH",
|
|
634
|
+
headers: buildIntelHeaders(cfg.controlToken, true),
|
|
635
|
+
body: JSON.stringify(body),
|
|
636
|
+
signal: controller.signal,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
endHttpSpan(span, { kind: "network_error", error: err });
|
|
641
|
+
throw err;
|
|
642
|
+
}
|
|
643
|
+
(0, observability_1.noteIntelEchoedTraceId)(res.headers.get("x-trace-id"));
|
|
644
|
+
const text = await res.text();
|
|
645
|
+
if (!res.ok) {
|
|
646
|
+
endHttpSpan(span, { kind: "http_error", status: res.status });
|
|
647
|
+
throw buildError(res.status, text, "PATCH", url);
|
|
648
|
+
}
|
|
649
|
+
endHttpSpan(span, { kind: "ok", status: res.status });
|
|
650
|
+
if (!text)
|
|
651
|
+
return {};
|
|
652
|
+
try {
|
|
653
|
+
return JSON.parse(text);
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
return { raw: text };
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
finally {
|
|
660
|
+
clearTimeout(timer);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
async function ping(cfg, path) {
|
|
664
|
+
try {
|
|
665
|
+
// allowUnauthenticated: doctor pings the UNAUTHENTICATED /internal/v1/health
|
|
666
|
+
// route to prove connectivity. A `mode: 'none'` config must not fail-fast
|
|
667
|
+
// here (there is genuinely no session, but the probe still answers), and the
|
|
668
|
+
// probe has no token to auto-refresh. Goes through doFetch directly to pass
|
|
669
|
+
// the bypass that `get` cannot express.
|
|
670
|
+
await doFetch(cfg, "GET", path, undefined, 5000, { allowUnauthenticated: true });
|
|
671
|
+
return { ok: true };
|
|
672
|
+
}
|
|
673
|
+
catch (e) {
|
|
674
|
+
const err = e;
|
|
675
|
+
return { ok: false, status: err.status, error: err.message };
|
|
676
|
+
}
|
|
677
|
+
}
|