@theronap/agnoclast-mcp 0.9.232 โ 0.9.233
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/locked_tools.mjs +102 -0
- package/lib/server.mjs +23 -2
- package/package.json +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// AGNOCLAST LITE โ LOCKED TOOLS on the stdio server (Theron, 2026-10-13 3:19 PM MDT: "What if we stopped the
|
|
2
|
+
// agent from using the tools but we did allow them to see that the tools are there as something that would be
|
|
3
|
+
// open to them if they either used the trial or paid for the plan?").
|
|
4
|
+
//
|
|
5
|
+
// The list, the ๐ line and every answer live on the server (web/lib/engine/locked_tools.ts) so the claude.ai
|
|
6
|
+
// connector and this server can never disagree. This module only:
|
|
7
|
+
// ยท asks once at start-up whether this account is locked (GET /api/lite/locked), and marks those tools'
|
|
8
|
+
// descriptions with the server's ๐ line before the first tools/list goes out โ the rest of each
|
|
9
|
+
// description is left exactly as written;
|
|
10
|
+
// ยท for a call to a locked tool, asks the server for the answer (POST /api/lite/locked) and returns it
|
|
11
|
+
// INSTEAD of running the tool. If the server now says "not locked" (the trial started mid-session), the
|
|
12
|
+
// tool runs as usual.
|
|
13
|
+
// ยท FAILS CLOSED when it cannot know (Codex review, 2026-10-13): if start-up could not learn the account's
|
|
14
|
+
// state, every tool in REGULAR_ONLY_TOOLS asks per call, and a call whose check cannot be made runs
|
|
15
|
+
// nothing and says so. A definite "not locked" (Regular) at start-up costs nothing per call.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The fallback list, used only when start-up could not ask the server. The same names as the server's
|
|
19
|
+
* LOCKED_TOOLS (web/lib/engine/locked_tools.ts) that exist here โ web/lib/engine/trial.test.ts pins the two.
|
|
20
|
+
*/
|
|
21
|
+
export const REGULAR_ONLY_TOOLS = [
|
|
22
|
+
'list_records', 'read_record', 'timeline_pull', 'record_status', 'set_record_notes', 'set_record_privacy',
|
|
23
|
+
'relabel_record_people', 'supersede_record', 'move_record', 'contain_record', 'pending_records', 'claim_record',
|
|
24
|
+
'route_record', 'unroute_record', 'request_file', 'get_file', 'create_custom_source', 'search_spam_email',
|
|
25
|
+
'gmail_modify', 'draft_email_reply', 'request_send_email', 'send_imessage', 'create_brain', 'add_to_brain',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
/** What a locked tool answers when the check itself could not be made. Nothing was run. */
|
|
29
|
+
export const LOCK_CHECK_FAILED =
|
|
30
|
+
'Not run: this tool is part of the Agnoclast Regular plan, and Agnoclast did not answer whether this account can use it right now, ' +
|
|
31
|
+
'so nothing was done. Try again in a moment.'
|
|
32
|
+
|
|
33
|
+
/** Start-up budget: the MCP handshake must not wait on this for long. */
|
|
34
|
+
export const LOCK_FETCH_TIMEOUT_MS = 2500
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* GET /api/lite/locked โ { known: true, locked: false } (Regular, or a server that predates locked tools: a
|
|
38
|
+
* 404) | { known: true, locked: true, tools, lockLine } | { known: false, locked: false } (no answer). Never throws.
|
|
39
|
+
*/
|
|
40
|
+
export async function fetchLockState(base, token, fetchImpl) {
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetchImpl(`${base}/api/lite/locked`, { headers: { Authorization: `Bearer ${token}` } }, { skipAutofile: true })
|
|
43
|
+
if (res.status === 404) return { known: true, locked: false }
|
|
44
|
+
if (!res.ok) return { known: false, locked: false }
|
|
45
|
+
const j = await res.json()
|
|
46
|
+
if (j?.locked === false) return { known: true, locked: false }
|
|
47
|
+
if (j?.locked !== true || !Array.isArray(j.tools) || typeof j.lockLine !== 'string') return { known: false, locked: false }
|
|
48
|
+
return { known: true, locked: true, tools: j.tools.filter((t) => typeof t === 'string'), lockLine: j.lockLine }
|
|
49
|
+
} catch {
|
|
50
|
+
return { known: false, locked: false }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The lock state, or "unknown" if it did not arrive within the budget. */
|
|
55
|
+
export async function lockStateWithin(promise, ms = LOCK_FETCH_TIMEOUT_MS) {
|
|
56
|
+
let timer
|
|
57
|
+
const timeout = new Promise((resolve) => { timer = setTimeout(() => resolve({ known: false, locked: false }), ms) })
|
|
58
|
+
try { return await Promise.race([promise, timeout]) } finally { clearTimeout(timer) }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Which registered tools must ask before they run, marking the locked ones' descriptions (lockLine + the
|
|
63
|
+
* original). Locked โ the server's list. Unknown โ REGULAR_ONLY_TOOLS, unmarked (we do not know they are
|
|
64
|
+
* locked; each call asks). Regular โ none. `handles` maps a name to { handle, description }.
|
|
65
|
+
*/
|
|
66
|
+
export function applyLock(handles, lock) {
|
|
67
|
+
const checked = new Set()
|
|
68
|
+
if (lock?.known === false) {
|
|
69
|
+
for (const name of REGULAR_ONLY_TOOLS) if (handles.has(name)) checked.add(name)
|
|
70
|
+
return checked
|
|
71
|
+
}
|
|
72
|
+
if (!lock?.locked) return checked
|
|
73
|
+
for (const name of lock.tools) {
|
|
74
|
+
const h = handles.get(name)
|
|
75
|
+
if (!h) continue
|
|
76
|
+
h.handle.update({ description: `${lock.lockLine}${h.description ?? ''}` })
|
|
77
|
+
checked.add(name)
|
|
78
|
+
}
|
|
79
|
+
return checked
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* POST /api/lite/locked { tool, args } โ { run: true } to run the tool as usual, or { run: false, text } to
|
|
84
|
+
* answer instead. A check that cannot be made answers LOCK_CHECK_FAILED: nothing runs. Never throws.
|
|
85
|
+
*/
|
|
86
|
+
export async function lockedAnswerFor(base, token, tool, args, fetchImpl) {
|
|
87
|
+
try {
|
|
88
|
+
const res = await fetchImpl(`${base}/api/lite/locked`, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
91
|
+
body: JSON.stringify({ tool, args: args && typeof args === 'object' && !Array.isArray(args) ? args : {} }),
|
|
92
|
+
}, { skipAutofile: true })
|
|
93
|
+
if (res.status === 404) return { run: true } // a server that predates locked tools locks nothing
|
|
94
|
+
if (!res.ok) return { run: false, text: LOCK_CHECK_FAILED }
|
|
95
|
+
const j = await res.json()
|
|
96
|
+
if (j?.locked === false) return { run: true }
|
|
97
|
+
if (j?.locked === true && typeof j.text === 'string') return { run: false, text: j.text }
|
|
98
|
+
return { run: false, text: LOCK_CHECK_FAILED }
|
|
99
|
+
} catch {
|
|
100
|
+
return { run: false, text: LOCK_CHECK_FAILED }
|
|
101
|
+
}
|
|
102
|
+
}
|
package/lib/server.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import { renderRecord, READ_RECORD_DEFAULT_CHARS, READ_RECORD_PARTS } from './re
|
|
|
30
30
|
import { formatCreated, formatDryRun, formatSources, homeFromArgs, pickSource, renderHomeProblem } from './custom_sources.mjs'
|
|
31
31
|
import { formatSeriesRefusal, formatSeriesUpdated } from './series_render.mjs'
|
|
32
32
|
import { formatSends } from './sends_render.mjs'
|
|
33
|
+
import { applyLock, fetchLockState, lockedAnswerFor, lockStateWithin } from './locked_tools.mjs'
|
|
33
34
|
|
|
34
35
|
// PAGE tiers (ADR-0071). Six for a PAGE; a RECORD keeps three (`privacy` on log_session and
|
|
35
36
|
// set_record_privacy below) โ an owner-relative tier is applied to a page's records when they are read
|
|
@@ -387,11 +388,30 @@ export async function runServer(version) {
|
|
|
387
388
|
}
|
|
388
389
|
|
|
389
390
|
const server = new McpServer({ name: 'cortex', version })
|
|
391
|
+
// AGNOCLAST LITE โ locked tools (lib/locked_tools.mjs; Theron, 2026-10-13 3:19 PM). Asked now, in parallel
|
|
392
|
+
// with registering ~120 tools, and awaited (bounded) just before the handshake, so the first tools/list a
|
|
393
|
+
// Lite account's AI sees already marks the Regular-only tools ๐. Regular accounts: nothing changes.
|
|
394
|
+
const lockP = fetchLockState(BASE, TOKEN, fetchCortex)
|
|
395
|
+
const toolHandles = new Map()
|
|
396
|
+
let lockedTools = new Set()
|
|
390
397
|
// ADR-0070: every tool runs with its NAME in async context, so an outage fetchCortex auto-files is
|
|
391
398
|
// attributed to the tool that hit it rather than guessed from a URL shared by several tools.
|
|
392
399
|
const registerTool = server.registerTool.bind(server)
|
|
393
|
-
server.registerTool = (name, config, handler) =>
|
|
394
|
-
|
|
400
|
+
server.registerTool = (name, config, handler) => {
|
|
401
|
+
const takesArgs = !!config?.inputSchema
|
|
402
|
+
const handle = registerTool(name, config, (...args) => runWithTool(name, async () => {
|
|
403
|
+
// A LOCKED tool runs nothing: the server's answer says what it would have done and how Regular is
|
|
404
|
+
// reached. "Not locked any more" (the trial started mid-session) โ the tool runs as usual; a check that
|
|
405
|
+
// cannot be made โ nothing runs (fails closed).
|
|
406
|
+
if (lockedTools.has(name)) {
|
|
407
|
+
const check = await lockedAnswerFor(BASE, TOKEN, name, takesArgs ? args[0] : {}, fetchCortex)
|
|
408
|
+
if (!check.run) return { content: [{ type: 'text', text: check.text }] }
|
|
409
|
+
}
|
|
410
|
+
return handler(...args)
|
|
411
|
+
}))
|
|
412
|
+
toolHandles.set(name, { handle, description: config?.description })
|
|
413
|
+
return handle
|
|
414
|
+
}
|
|
395
415
|
|
|
396
416
|
|
|
397
417
|
// โโ THE PRE-CLAIM NUDGE, rendered โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
@@ -6710,5 +6730,6 @@ function renderNudge(payload) {
|
|
|
6710
6730
|
},
|
|
6711
6731
|
)
|
|
6712
6732
|
|
|
6733
|
+
lockedTools = applyLock(toolHandles, await lockStateWithin(lockP))
|
|
6713
6734
|
await server.connect(new StdioServerTransport())
|
|
6714
6735
|
}
|
package/package.json
CHANGED