@theronap/cortex-mcp 0.9.113 → 0.9.115
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/lib/context_log.mjs +6 -1
- package/lib/diagnose.mjs +20 -2
- package/lib/server.mjs +17 -4
- package/package.json +1 -1
package/lib/context_log.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlink
|
|
|
2
2
|
import { homedir } from 'os'
|
|
3
3
|
import { join } from 'path'
|
|
4
4
|
import { fetchCortex, classify, resolveBase, resolveTokenSource } from './diagnose.mjs'
|
|
5
|
+
import { repoFullNameFrom } from './capture.mjs'
|
|
5
6
|
|
|
6
7
|
// The private third copy of this lived here until 2026-08-19. doctor.mjs:14 warned that a third
|
|
7
8
|
// copy is how a machine ends up connected to one command and 'no token found' to another; it also
|
|
@@ -47,9 +48,13 @@ export async function runSnapshotContext() {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
const base = resolveBase(process.env.CORTEX_URL)
|
|
51
|
+
// The SessionStart snapshot is the surface that actually reaches a session's first turn, so the
|
|
52
|
+
// repo-scoped Gate 4 block is worth more here than anywhere else. Fail-closed: a non-GitHub cwd
|
|
53
|
+
// yields null and the hint is omitted (see repoFullNameFrom).
|
|
54
|
+
const repo = repoFullNameFrom(process.cwd())
|
|
50
55
|
let res
|
|
51
56
|
try {
|
|
52
|
-
res = await fetchCortex(`${base}/api/mcp-context`, { headers: { Authorization: `Bearer ${token}` } })
|
|
57
|
+
res = await fetchCortex(`${base}/api/mcp-context${repo ? `?repo=${encodeURIComponent(repo)}` : ''}`, { headers: { Authorization: `Bearer ${token}` } })
|
|
53
58
|
} catch (e) {
|
|
54
59
|
out(`Agnoclast: context snapshot failed — ${e?.message ?? String(e)}`)
|
|
55
60
|
return 0
|
package/lib/diagnose.mjs
CHANGED
|
@@ -238,11 +238,29 @@ export function classify(status, contentType, bodyText, requestId) {
|
|
|
238
238
|
let SESSION_KEY = null
|
|
239
239
|
export function setSessionKey(key) { SESSION_KEY = key || null }
|
|
240
240
|
|
|
241
|
+
// WHO IS CALLING — an identity INDEPENDENT of the session key, and it must stay independent.
|
|
242
|
+
//
|
|
243
|
+
// The server labels every page revision's `actor_source` from this header. It used to default to
|
|
244
|
+
// 'mcp' server-side with nothing setting it, so 'mcp' silently meant "unlabelled" and an ADR-0030
|
|
245
|
+
// scrub run against the HTTP API on 2026-08-26 wrote 340 revisions that read as MCP sessions with
|
|
246
|
+
// dropped attribution — which reopened gate 2 on 2026-08-27 against a cause that was never MCP.
|
|
247
|
+
//
|
|
248
|
+
// ⚠ Deliberately NOT derived from SESSION_KEY, and deliberately sent even when SESSION_KEY is null.
|
|
249
|
+
// The alarm's whole subject is `actor_source='mcp' AND session_key IS NULL`; if this header were
|
|
250
|
+
// conditional on the session, that pair could never occur and the detector would be structurally
|
|
251
|
+
// blind. A one-shot hook process legitimately has no session and IS still cortex-mcp — it should
|
|
252
|
+
// say so, and be visible as one, not disguise itself as a script.
|
|
253
|
+
let CLIENT_ID = 'cortex-mcp/unknown'
|
|
254
|
+
export function setClientVersion(version) {
|
|
255
|
+
CLIENT_ID = `cortex-mcp/${version || 'unknown'}`
|
|
256
|
+
}
|
|
257
|
+
|
|
241
258
|
export async function fetchCortex(url, opts = {}, { retries = 2, baseDelayMs = 400 } = {}) {
|
|
242
259
|
const { timeoutMs: optTimeout, signal: callerSignal, ...rest } = opts
|
|
260
|
+
const baseHeaders = { ...(rest.headers ?? {}), 'x-cortex-client': CLIENT_ID }
|
|
243
261
|
const fetchOpts = SESSION_KEY
|
|
244
|
-
? { ...rest, headers: { ...
|
|
245
|
-
: rest
|
|
262
|
+
? { ...rest, headers: { ...baseHeaders, 'x-cortex-session-key': SESSION_KEY } }
|
|
263
|
+
: { ...rest, headers: baseHeaders }
|
|
246
264
|
const timeoutMs = optTimeout ?? (Number(process.env.CORTEX_HTTP_TIMEOUT_MS) || 15_000)
|
|
247
265
|
let lastErr
|
|
248
266
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
package/lib/server.mjs
CHANGED
|
@@ -5,13 +5,14 @@ import { writeFileSync, mkdirSync } from 'fs'
|
|
|
5
5
|
import { homedir } from 'os'
|
|
6
6
|
import { join } from 'path'
|
|
7
7
|
import { createHash, randomUUID } from 'crypto'
|
|
8
|
-
import { fetchCortex, classify, resolveBase, resolveEnvToken, setSessionKey } from './diagnose.mjs'
|
|
8
|
+
import { fetchCortex, classify, resolveBase, resolveEnvToken, setSessionKey, setClientVersion } from './diagnose.mjs'
|
|
9
9
|
import { folderClaimLine } from './folder_claims.mjs'
|
|
10
10
|
import { resolveSessionKey, resolveLogSessionId } from './session_key.mjs'
|
|
11
11
|
import { runSendImessage } from './imessage_send.mjs'
|
|
12
12
|
import { formatGrepHits } from './grep_cli.mjs'
|
|
13
13
|
import { renderTriage } from './red_link_triage.mjs'
|
|
14
14
|
import { runCodeGraphQuery } from './code_graph_cli.mjs'
|
|
15
|
+
import { repoFullNameFrom } from './capture.mjs'
|
|
15
16
|
|
|
16
17
|
// Reactive red-link triage (Mechanism 2). On a read_page miss, ask the server whether the name is a
|
|
17
18
|
// tracked wanted page, whether a bare node exists for it, and whether it's a deliberately demoted page,
|
|
@@ -104,6 +105,10 @@ export async function runServer(version) {
|
|
|
104
105
|
// resolve THIS session's brain pointer. Must be set BEFORE the first fetchCortex call below —
|
|
105
106
|
// otherwise the opening requests of a session would silently resolve by the account pointer.
|
|
106
107
|
setSessionKey(SESSION_KEY)
|
|
108
|
+
// Paired with setSessionKey and set at the same moment, but a SEPARATE signal on purpose: the
|
|
109
|
+
// server labels actor_source from this and watches for `mcp` + no session. Tying the two together
|
|
110
|
+
// would make that pair impossible and blind the detector. See actorSourceFrom in auth_cloud.ts.
|
|
111
|
+
setClientVersion(version)
|
|
107
112
|
async function pingSession() {
|
|
108
113
|
try {
|
|
109
114
|
await fetchCortex(`${BASE}/api/session-ping`, {
|
|
@@ -122,19 +127,27 @@ export async function runServer(version) {
|
|
|
122
127
|
if (typeof _hb.unref === 'function') _hb.unref() // don't keep the process alive just for the heartbeat
|
|
123
128
|
|
|
124
129
|
// Cache context for 5 minutes so repeated tool calls don't re-fetch.
|
|
130
|
+
// KEYED BY REPO since 2026-08-27: the context now carries a repo-scoped Gate 4 block, so the same
|
|
131
|
+
// token can legitimately get different context from different cwds. One stdio server serves one cwd
|
|
132
|
+
// today and this is belt-and-braces — but an unkeyed cache would serve one repo's block to another
|
|
133
|
+
// silently, and "your repo has 12 records waiting" pointing at the wrong repo is worse than no line.
|
|
125
134
|
let cache = null
|
|
126
135
|
async function fetchContext() {
|
|
127
136
|
const now = Date.now()
|
|
128
|
-
|
|
137
|
+
// Fail-closed by construction (see repoFullNameFrom): a non-GitHub or non-repo cwd yields null and
|
|
138
|
+
// the request simply omits the hint, degrading to the generic block.
|
|
139
|
+
const repo = repoFullNameFrom(process.cwd())
|
|
140
|
+
if (cache && cache.repo === repo && now - cache.ts < 5 * 60 * 1000) return cache.text
|
|
129
141
|
// fetchCortex retries transient infra/5xx; classify turns a failure into an honest message
|
|
130
142
|
// (token vs infra-block vs network) instead of a bare "Agnoclast API 403: unknown".
|
|
131
|
-
const
|
|
143
|
+
const url = `${BASE}/api/mcp-context${repo ? `?repo=${encodeURIComponent(repo)}` : ''}`
|
|
144
|
+
const res = await fetchCortex(url, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
132
145
|
if (!res.ok) {
|
|
133
146
|
const body = await res.text()
|
|
134
147
|
throw new Error(classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message)
|
|
135
148
|
}
|
|
136
149
|
const { context, brainRefs } = await res.json()
|
|
137
|
-
cache = { text: context, ts: now }
|
|
150
|
+
cache = { text: context, ts: now, repo }
|
|
138
151
|
// T11: stash the digest node refs surfaced this fetch so capture.mjs can forward them as
|
|
139
152
|
// hydrated_from at the session's ingest (feedback-loop guard). Keyed by cwd so the matching
|
|
140
153
|
// session picks them up. Best-effort + inert when brainRefs is empty (digest flag off).
|
package/package.json
CHANGED