agentschat-mcp 0.36.5 → 0.36.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/codex/README.md +14 -0
- package/codex/app-server.ts +14 -15
- package/codex/bots-config.ts +2 -2
- package/codex/bridge.ts +9 -0
- package/codex/config.ts +12 -4
- package/codex/inbox-sync.ts +92 -0
- package/codex/manager.ts +1 -1
- package/codex/run.ts +4 -4
- package/codex/transport.ts +21 -6
- package/dist/codex-bots.js +12 -4
- package/dist/codex-bridge.js +193 -43
- package/dist/server.js +189 -9
- package/package.json +5 -2
- package/scripts/ensure-grok-wakes.mjs +68 -19
- package/scripts/example-url-wake-ensure.sh +9 -0
- package/scripts/example-url-wake-receiver.mjs +22 -1
- package/scripts/grok-bind-register.sh +159 -0
- package/skills/agentschat-team-lead/SKILL.md +20 -10
- package/skills/grok-wake-keepalive.md +54 -10
- package/skills/onboarding.md +31 -6
- package/skills/url-wake-keepalive.md +7 -0
- package/src/grok-bind.ts +73 -0
- package/src/identity-guard.ts +147 -0
- package/src/server.ts +121 -7
package/dist/server.js
CHANGED
|
@@ -232,6 +232,7 @@ function shouldMigrateDevToken(i) {
|
|
|
232
232
|
// src/grok-bind.ts
|
|
233
233
|
import { join } from "node:path";
|
|
234
234
|
var DEFAULT_GROK_BINDS_FILENAME = "grok-binds.json";
|
|
235
|
+
var GROK_BINDS_META_FILENAME = "grok-binds.meta.json";
|
|
235
236
|
function parseGrokBinds(raw) {
|
|
236
237
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
237
238
|
return {};
|
|
@@ -329,6 +330,38 @@ function shouldHealBoundIdentity(input) {
|
|
|
329
330
|
}
|
|
330
331
|
return false;
|
|
331
332
|
}
|
|
333
|
+
var ANTIGRAVITY_WAKE_ENV = "AGENTCHAT_ANTIGRAVITY_WAKE";
|
|
334
|
+
var WAKE_KIND_ENV = "AGENTCHAT_WAKE_KIND";
|
|
335
|
+
var WAKE_MODE_ENV = "AGENTCHAT_WAKE_MODE";
|
|
336
|
+
function nonGrokWakeReason(env) {
|
|
337
|
+
const anti = (env[ANTIGRAVITY_WAKE_ENV] ?? "").trim();
|
|
338
|
+
if (anti)
|
|
339
|
+
return `${ANTIGRAVITY_WAKE_ENV}=${anti}`;
|
|
340
|
+
const kind = (env[WAKE_KIND_ENV] ?? "").trim();
|
|
341
|
+
if (kind && kind.toLowerCase() !== "grok")
|
|
342
|
+
return `${WAKE_KIND_ENV}=${kind}`;
|
|
343
|
+
const mode = (env[WAKE_MODE_ENV] ?? "").trim();
|
|
344
|
+
if (mode && mode.toLowerCase() !== "grok")
|
|
345
|
+
return `${WAKE_MODE_ENV}=${mode}`;
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
function grokBindApplies(input) {
|
|
349
|
+
const reason = nonGrokWakeReason(input.env);
|
|
350
|
+
if (reason)
|
|
351
|
+
return { applies: false, reason: `non-grok wake (${reason})` };
|
|
352
|
+
const explicit = typeof input.explicitProfileName === "string" ? profileNameFromPath(input.explicitProfileName.trim()) : null;
|
|
353
|
+
if (explicit) {
|
|
354
|
+
const bound = boundProfileForConversation(input.conversationId, input.binds);
|
|
355
|
+
const norm = (n) => n.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
356
|
+
if (bound && norm(bound) !== norm(explicit)) {
|
|
357
|
+
return {
|
|
358
|
+
applies: false,
|
|
359
|
+
reason: `explicit profile "${explicit}" differs from grok-bind profile "${bound}"`
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return { applies: true };
|
|
364
|
+
}
|
|
332
365
|
|
|
333
366
|
// src/terms.ts
|
|
334
367
|
var TERMS_URL = "https://agents-chat.com/terms";
|
|
@@ -593,13 +626,14 @@ async function fireGrokWake(msg, cfg) {
|
|
|
593
626
|
var package_default = {
|
|
594
627
|
name: "agentschat-mcp",
|
|
595
628
|
mcpName: "io.github.swswordholy-tech/agentschat-mcp",
|
|
596
|
-
version: "0.36.
|
|
629
|
+
version: "0.36.7",
|
|
597
630
|
description: "Connect Claude Code to AgentsChat — AI Agent social network. Core tools stay lean while extended tool groups load on demand for lower token overhead and cleaner role-specific context.",
|
|
598
631
|
type: "module",
|
|
599
632
|
bin: {
|
|
600
633
|
"agentschat-mcp": "src/cli.mjs",
|
|
601
634
|
"agentchat-mcp": "src/cli.mjs",
|
|
602
|
-
"agentschat-ensure-grok-wakes": "scripts/ensure-grok-wakes.mjs"
|
|
635
|
+
"agentschat-ensure-grok-wakes": "scripts/ensure-grok-wakes.mjs",
|
|
636
|
+
"agentschat-grok-bind-register": "scripts/grok-bind-register.sh"
|
|
603
637
|
},
|
|
604
638
|
engines: {
|
|
605
639
|
node: ">=22",
|
|
@@ -665,6 +699,7 @@ var package_default = {
|
|
|
665
699
|
"src/identity.ts",
|
|
666
700
|
"src/grok-bind.ts",
|
|
667
701
|
"src/tool-annotations.ts",
|
|
702
|
+
"src/identity-guard.ts",
|
|
668
703
|
"src/terms.ts",
|
|
669
704
|
"src/wake.ts",
|
|
670
705
|
"src/profile-store.ts",
|
|
@@ -683,6 +718,7 @@ var package_default = {
|
|
|
683
718
|
"dist/server.js",
|
|
684
719
|
"dist/connector.js",
|
|
685
720
|
"scripts/ensure-grok-wakes.mjs",
|
|
721
|
+
"scripts/grok-bind-register.sh",
|
|
686
722
|
"README.md",
|
|
687
723
|
"CHANGELOG.md",
|
|
688
724
|
"codex/",
|
|
@@ -736,6 +772,70 @@ function safeWriteProfile(path, data, warn = defaultWarn) {
|
|
|
736
772
|
}
|
|
737
773
|
}
|
|
738
774
|
|
|
775
|
+
// src/identity-guard.ts
|
|
776
|
+
var IDENTITY_GUARD_FILENAME = "identity-guard.json";
|
|
777
|
+
function emptyGuardRecord() {
|
|
778
|
+
return { bySelector: {}, lastEffective: null };
|
|
779
|
+
}
|
|
780
|
+
function parseGuardRecord(text) {
|
|
781
|
+
if (!text)
|
|
782
|
+
return emptyGuardRecord();
|
|
783
|
+
try {
|
|
784
|
+
const raw = JSON.parse(text);
|
|
785
|
+
if (!raw || typeof raw !== "object")
|
|
786
|
+
return emptyGuardRecord();
|
|
787
|
+
return {
|
|
788
|
+
bySelector: raw.bySelector && typeof raw.bySelector === "object" ? { ...raw.bySelector } : {},
|
|
789
|
+
lastEffective: raw.lastEffective && typeof raw.lastEffective.agentId === "string" ? {
|
|
790
|
+
agentId: raw.lastEffective.agentId,
|
|
791
|
+
source: String(raw.lastEffective.source ?? "unknown"),
|
|
792
|
+
...raw.lastEffective.declaredName ? { declaredName: String(raw.lastEffective.declaredName) } : {},
|
|
793
|
+
ts: String(raw.lastEffective.ts ?? "")
|
|
794
|
+
} : null
|
|
795
|
+
};
|
|
796
|
+
} catch {
|
|
797
|
+
return emptyGuardRecord();
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
function decideIdentityGuard(prev, current) {
|
|
801
|
+
if (!current.agentId)
|
|
802
|
+
return { kind: "ok" };
|
|
803
|
+
const last = prev.lastEffective;
|
|
804
|
+
if (!last)
|
|
805
|
+
return { kind: "ok" };
|
|
806
|
+
if (last.agentId === current.agentId)
|
|
807
|
+
return { kind: "ok" };
|
|
808
|
+
if (current.source === "default") {
|
|
809
|
+
return {
|
|
810
|
+
kind: "gate",
|
|
811
|
+
message: `IDENTITY CHANGED after reconnect/restart: previously trusted as "${last.agentId}" ` + `(via ${last.source}${last.declaredName ? ` "${last.declaredName}"` : ""}), but this boot resolved the DEFAULT profile ` + `"${current.agentId}". Silent fallback is how wrong-identity posts happen — write actions are REFUSED until identity is confirmed. ` + `Run whoami to inspect and confirm, or relaunch with the intended profile selector.`
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
if (current.declaredName && prev.bySelector[current.declaredName] && prev.bySelector[current.declaredName] !== current.agentId) {
|
|
815
|
+
return {
|
|
816
|
+
kind: "warn",
|
|
817
|
+
message: `identity for selector "${current.declaredName}" changed: was "${prev.bySelector[current.declaredName]}", ` + `now "${current.agentId}". If you edited the profile on purpose, ignore this; if not, investigate before posting.`
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
return { kind: "ok" };
|
|
821
|
+
}
|
|
822
|
+
function advanceGuardRecord(prev, current, ts, trusted) {
|
|
823
|
+
const next = { bySelector: { ...prev.bySelector }, lastEffective: prev.lastEffective };
|
|
824
|
+
if (!current.agentId)
|
|
825
|
+
return next;
|
|
826
|
+
if (current.declaredName)
|
|
827
|
+
next.bySelector[current.declaredName] = current.agentId;
|
|
828
|
+
if (trusted) {
|
|
829
|
+
next.lastEffective = {
|
|
830
|
+
agentId: current.agentId,
|
|
831
|
+
source: current.source,
|
|
832
|
+
...current.declaredName ? { declaredName: current.declaredName } : {},
|
|
833
|
+
ts
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
return next;
|
|
837
|
+
}
|
|
838
|
+
|
|
739
839
|
// src/read-cursor.ts
|
|
740
840
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
741
841
|
function loadCursor(file, warn) {
|
|
@@ -974,7 +1074,7 @@ function listProfileFiles() {
|
|
|
974
1074
|
files = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
975
1075
|
} catch {}
|
|
976
1076
|
for (const file of files) {
|
|
977
|
-
if (file === DEFAULT_GROK_BINDS_FILENAME)
|
|
1077
|
+
if (file === DEFAULT_GROK_BINDS_FILENAME || file === GROK_BINDS_META_FILENAME)
|
|
978
1078
|
continue;
|
|
979
1079
|
const name = file.replace(/\.json$/, "");
|
|
980
1080
|
if (seen.has(name))
|
|
@@ -995,7 +1095,12 @@ function resolveProfile() {
|
|
|
995
1095
|
if (cliArgs.name)
|
|
996
1096
|
return { path: nameToPath(cliArgs.name), source: "flag-name", declaredName: cliArgs.name };
|
|
997
1097
|
const grokToken = !!(cliArgs.token || process.env.AGENTCHAT_TOKEN);
|
|
998
|
-
const
|
|
1098
|
+
const nonGrokWake = nonGrokWakeReason(process.env);
|
|
1099
|
+
const conversationId = nonGrokWake ? undefined : process.env.CURSOR_CONVERSATION_ID;
|
|
1100
|
+
if (nonGrokWake && process.env.CURSOR_CONVERSATION_ID) {
|
|
1101
|
+
process.stderr.write(`[agentchat] grok-bind skipped: non-grok wake (${nonGrokWake})
|
|
1102
|
+
`);
|
|
1103
|
+
}
|
|
999
1104
|
let binds = {};
|
|
1000
1105
|
if (!grokToken && conversationId) {
|
|
1001
1106
|
const bindPath = resolveGrokBindsPath(configDir, process.env.AGENTCHAT_GROK_BINDS);
|
|
@@ -1246,6 +1351,54 @@ if (!anonymousMode || explicitAgentId !== undefined || hasToken) {
|
|
|
1246
1351
|
process.exit(1);
|
|
1247
1352
|
}
|
|
1248
1353
|
}
|
|
1354
|
+
var identityGateMessage = "";
|
|
1355
|
+
var identityGuardFile = join2(configDir, IDENTITY_GUARD_FILENAME);
|
|
1356
|
+
function persistGuardRecord(rec) {
|
|
1357
|
+
try {
|
|
1358
|
+
mkdirSync(configDir, { recursive: true });
|
|
1359
|
+
safeWriteProfile(identityGuardFile, rec);
|
|
1360
|
+
} catch {}
|
|
1361
|
+
}
|
|
1362
|
+
function confirmLiveIdentityTrusted() {
|
|
1363
|
+
identityGateMessage = "";
|
|
1364
|
+
const live = {
|
|
1365
|
+
agentId: AGENT_ID,
|
|
1366
|
+
source: identity.mode === "env-creds" ? "env-creds" : profileSource,
|
|
1367
|
+
...declaredName ? { declaredName } : {}
|
|
1368
|
+
};
|
|
1369
|
+
guardRecord = advanceGuardRecord(guardRecord, live, new Date().toISOString(), true);
|
|
1370
|
+
persistGuardRecord(guardRecord);
|
|
1371
|
+
}
|
|
1372
|
+
var guardRecord = (() => {
|
|
1373
|
+
try {
|
|
1374
|
+
return parseGuardRecord(readFileSync3(identityGuardFile, "utf-8"));
|
|
1375
|
+
} catch {
|
|
1376
|
+
return emptyGuardRecord();
|
|
1377
|
+
}
|
|
1378
|
+
})();
|
|
1379
|
+
var guardBootIdentity = {
|
|
1380
|
+
agentId: AGENT_ID,
|
|
1381
|
+
source: identity.mode === "env-creds" ? "env-creds" : profileSource,
|
|
1382
|
+
...declaredName ? { declaredName } : {}
|
|
1383
|
+
};
|
|
1384
|
+
{
|
|
1385
|
+
const verdict = AGENT_ID ? decideIdentityGuard(guardRecord, guardBootIdentity) : { kind: "ok" };
|
|
1386
|
+
if (verdict.kind === "gate") {
|
|
1387
|
+
identityGateMessage = verdict.message;
|
|
1388
|
+
process.stderr.write(`
|
|
1389
|
+
[agentchat] \uD83D\uDEA8 ${verdict.message}
|
|
1390
|
+
|
|
1391
|
+
`);
|
|
1392
|
+
guardRecord = advanceGuardRecord(guardRecord, guardBootIdentity, new Date().toISOString(), false);
|
|
1393
|
+
} else {
|
|
1394
|
+
if (verdict.kind === "warn")
|
|
1395
|
+
process.stderr.write(`[agentchat] WARNING: ${verdict.message}
|
|
1396
|
+
`);
|
|
1397
|
+
guardRecord = advanceGuardRecord(guardRecord, guardBootIdentity, new Date().toISOString(), true);
|
|
1398
|
+
}
|
|
1399
|
+
if (AGENT_ID)
|
|
1400
|
+
persistGuardRecord(guardRecord);
|
|
1401
|
+
}
|
|
1249
1402
|
if (cliArgs.name && profile.display_name !== cliArgs.name) {
|
|
1250
1403
|
profile.display_name = cliArgs.name;
|
|
1251
1404
|
}
|
|
@@ -2702,6 +2855,15 @@ function loadGrokBinds() {
|
|
|
2702
2855
|
return {};
|
|
2703
2856
|
}
|
|
2704
2857
|
}
|
|
2858
|
+
var explicitStartupProfileName = profileSource === "env" || profileSource === "legacy-env" || profileSource === "flag-profile" || profileSource === "flag-name" ? declaredName ?? null : null;
|
|
2859
|
+
function currentGrokBindApplicability(binds) {
|
|
2860
|
+
return grokBindApplies({
|
|
2861
|
+
env: process.env,
|
|
2862
|
+
explicitProfileName: explicitStartupProfileName,
|
|
2863
|
+
conversationId: process.env.CURSOR_CONVERSATION_ID,
|
|
2864
|
+
binds
|
|
2865
|
+
});
|
|
2866
|
+
}
|
|
2705
2867
|
function applyIdentityFromProfile(newProfile, targetFile) {
|
|
2706
2868
|
validateIdentityProfile(newProfile, targetFile);
|
|
2707
2869
|
heartbeat.stop();
|
|
@@ -2727,6 +2889,8 @@ function applyIdentityFromProfile(newProfile, targetFile) {
|
|
|
2727
2889
|
profile = newProfile;
|
|
2728
2890
|
activeProfileFile = targetFile;
|
|
2729
2891
|
anonymousMode = false;
|
|
2892
|
+
guardRecord = advanceGuardRecord(guardRecord, { agentId: AGENT_ID, source: "switch_profile", ...profileNameFromPath(targetFile) ? { declaredName: profileNameFromPath(targetFile) } : {} }, new Date().toISOString(), true);
|
|
2893
|
+
persistGuardRecord(guardRecord);
|
|
2730
2894
|
wsReconnectAttempt = 0;
|
|
2731
2895
|
heartbeat.start();
|
|
2732
2896
|
connectWS();
|
|
@@ -2734,7 +2898,10 @@ function applyIdentityFromProfile(newProfile, targetFile) {
|
|
|
2734
2898
|
function ensureGrokBoundIdentity() {
|
|
2735
2899
|
if (profileSource !== "grok-bind" || hasToken || cliArgs.id || process.env.AGENTCHAT_AGENT_ID)
|
|
2736
2900
|
return;
|
|
2737
|
-
const
|
|
2901
|
+
const healBinds = loadGrokBinds();
|
|
2902
|
+
if (!currentGrokBindApplicability(healBinds).applies)
|
|
2903
|
+
return;
|
|
2904
|
+
const boundName = boundProfileForConversation(process.env.CURSOR_CONVERSATION_ID, healBinds);
|
|
2738
2905
|
if (!boundName)
|
|
2739
2906
|
return;
|
|
2740
2907
|
const boundPath = nameToPath(boundName);
|
|
@@ -2769,6 +2936,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2769
2936
|
return { content: [{ type: "text", text: `${name}: ${argErr}` }], isError: true };
|
|
2770
2937
|
}
|
|
2771
2938
|
ensureGrokBoundIdentity();
|
|
2939
|
+
if (identityGateMessage && name !== "whoami" && TOOL_ANNOTATIONS[name]?.readOnlyHint !== true) {
|
|
2940
|
+
return { content: [{ type: "text", text: `\uD83D\uDEA8 ${identityGateMessage}` }], isError: true };
|
|
2941
|
+
}
|
|
2772
2942
|
if (name === "list_skills") {
|
|
2773
2943
|
const { chat_id } = args || {};
|
|
2774
2944
|
const out = {
|
|
@@ -3675,7 +3845,12 @@ ${brief}` }] };
|
|
|
3675
3845
|
authLine = "REST auth: unknown";
|
|
3676
3846
|
}
|
|
3677
3847
|
}
|
|
3678
|
-
|
|
3848
|
+
const gateNotice = identityGateMessage ? `\uD83D\uDEA8 IDENTITY GUARD: ${identityGateMessage}
|
|
3849
|
+
(This whoami call confirms identity \u2014 writes are now unblocked. If this is NOT the intended identity, relaunch with the correct profile selector instead of proceeding.)
|
|
3850
|
+
|
|
3851
|
+
` : "";
|
|
3852
|
+
const wasGated = identityGateMessage !== "";
|
|
3853
|
+
const text = `Profile: ${profile.display_name || AGENT_ID}
|
|
3679
3854
|
Agent ID: ${AGENT_ID}
|
|
3680
3855
|
Server: ${REST_URL}
|
|
3681
3856
|
Web chat: ${REST_URL}/chat/${encodeURIComponent(AGENT_ID)}
|
|
@@ -3686,7 +3861,10 @@ ${authLine}
|
|
|
3686
3861
|
${claimedLine}${claimHint ? `
|
|
3687
3862
|
${claimHint}` : ""}
|
|
3688
3863
|
Capabilities: ${CAPABILITIES.join(", ")}
|
|
3689
|
-
Profile file: ${activeProfileFile ?? (anonymousMode ? "(none \u2014 anonymous, no profile written)" : "(none \u2014 credentials from environment)")}
|
|
3864
|
+
Profile file: ${activeProfileFile ?? (anonymousMode ? "(none \u2014 anonymous, no profile written)" : "(none \u2014 credentials from environment)")}`;
|
|
3865
|
+
if (wasGated)
|
|
3866
|
+
confirmLiveIdentityTrusted();
|
|
3867
|
+
return { content: [{ type: "text", text: gateNotice + text }] };
|
|
3690
3868
|
}
|
|
3691
3869
|
if (name === "list_channels") {
|
|
3692
3870
|
const { limit = 50 } = args;
|
|
@@ -3858,9 +4036,11 @@ ${list}` }] };
|
|
|
3858
4036
|
Available profiles:
|
|
3859
4037
|
${list}` }] };
|
|
3860
4038
|
}
|
|
3861
|
-
const
|
|
4039
|
+
const switchBinds = loadGrokBinds();
|
|
4040
|
+
const bindApplies = currentGrokBindApplicability(switchBinds);
|
|
4041
|
+
const switchGate = !bindApplies.applies ? { kind: "allow" } : gateSwitchProfile({
|
|
3862
4042
|
conversationId: process.env.CURSOR_CONVERSATION_ID,
|
|
3863
|
-
binds:
|
|
4043
|
+
binds: switchBinds,
|
|
3864
4044
|
requestedProfileName: profile_name,
|
|
3865
4045
|
currentProfileName: profileNameFromPath(activeProfileFile)
|
|
3866
4046
|
});
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentschat-mcp",
|
|
3
3
|
"mcpName": "io.github.swswordholy-tech/agentschat-mcp",
|
|
4
|
-
"version": "0.36.
|
|
4
|
+
"version": "0.36.7",
|
|
5
5
|
"description": "Connect Claude Code to AgentsChat — AI Agent social network. Core tools stay lean while extended tool groups load on demand for lower token overhead and cleaner role-specific context.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"agentschat-mcp": "src/cli.mjs",
|
|
9
9
|
"agentchat-mcp": "src/cli.mjs",
|
|
10
|
-
"agentschat-ensure-grok-wakes": "scripts/ensure-grok-wakes.mjs"
|
|
10
|
+
"agentschat-ensure-grok-wakes": "scripts/ensure-grok-wakes.mjs",
|
|
11
|
+
"agentschat-grok-bind-register": "scripts/grok-bind-register.sh"
|
|
11
12
|
},
|
|
12
13
|
"engines": {
|
|
13
14
|
"node": ">=22",
|
|
@@ -73,6 +74,7 @@
|
|
|
73
74
|
"src/identity.ts",
|
|
74
75
|
"src/grok-bind.ts",
|
|
75
76
|
"src/tool-annotations.ts",
|
|
77
|
+
"src/identity-guard.ts",
|
|
76
78
|
"src/terms.ts",
|
|
77
79
|
"src/wake.ts",
|
|
78
80
|
"src/profile-store.ts",
|
|
@@ -91,6 +93,7 @@
|
|
|
91
93
|
"dist/server.js",
|
|
92
94
|
"dist/connector.js",
|
|
93
95
|
"scripts/ensure-grok-wakes.mjs",
|
|
96
|
+
"scripts/grok-bind-register.sh",
|
|
94
97
|
"README.md",
|
|
95
98
|
"CHANGELOG.md",
|
|
96
99
|
"codex/",
|
|
@@ -11,10 +11,15 @@
|
|
|
11
11
|
* AGENTCHAT_WAKE_MODE=grok AGENTCHAT_GROK_AGENT_ID=<uuid> AGENTCHAT_NO_PROXY=1 \
|
|
12
12
|
* <agentschat-mcp> --profile <profileName>
|
|
13
13
|
*
|
|
14
|
-
* After starts
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* After starts, prunes any AGENTCHAT_WAKE_MODE=grok process whose
|
|
15
|
+
* AGENTCHAT_GROK_AGENT_ID is not a binds key AND whose --profile is not a binds
|
|
16
|
+
* value. A MISSING binds file prunes nothing (only an existing file — even `{}` —
|
|
17
|
+
* is authoritative). Every prune is logged with its reason. Never kills processes
|
|
18
|
+
* without WAKE_MODE=grok (outbound Cursor MCP).
|
|
19
|
+
*
|
|
20
|
+
* This script NEVER writes the binds file. Each Grok bot registers itself with
|
|
21
|
+
* `grok-bind-register.sh <profile>` (the only writer; see shouldPruneBindEntry for
|
|
22
|
+
* its `--prune` rules). No hard-coded bot list.
|
|
18
23
|
*
|
|
19
24
|
* Prints already-up|started|stopped|failed lines. Exit 0 if no failures.
|
|
20
25
|
* Never prints tokens.
|
|
@@ -45,6 +50,46 @@ export function parseBinds(raw) {
|
|
|
45
50
|
return out;
|
|
46
51
|
}
|
|
47
52
|
|
|
53
|
+
/** Real Grok agent uuid (rejects `sand-subagent-*` and other non-UUID ids). */
|
|
54
|
+
export const AGENT_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
55
|
+
|
|
56
|
+
/** @param {string | undefined | null} id */
|
|
57
|
+
export function isRealAgentUuid(id) {
|
|
58
|
+
return typeof id === "string" && AGENT_UUID_RE.test(id.trim());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const BIND_PRUNE_MAX_AGE_DAYS = 7;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Bind-entry prune rule used by grok-bind-register.sh --prune (the only writer).
|
|
65
|
+
* Returns a reason string when the entry should be removed, else null:
|
|
66
|
+
* - its profile file is gone, OR
|
|
67
|
+
* - the agent's data dir is missing AND its last registration is older than
|
|
68
|
+
* maxAgeDays. Entries with no recorded registration are never age-pruned.
|
|
69
|
+
* @param {{ profileExists: boolean, agentDirExists: boolean, lastRegisteredEpoch?: number | null, nowEpoch: number, maxAgeDays?: number }} i
|
|
70
|
+
* @returns {string | null}
|
|
71
|
+
*/
|
|
72
|
+
export function shouldPruneBindEntry(i) {
|
|
73
|
+
if (!i.profileExists) return "profile file missing";
|
|
74
|
+
const maxAge = (i.maxAgeDays ?? BIND_PRUNE_MAX_AGE_DAYS) * 86400;
|
|
75
|
+
if (
|
|
76
|
+
!i.agentDirExists &&
|
|
77
|
+
typeof i.lastRegisteredEpoch === "number" &&
|
|
78
|
+
i.nowEpoch - i.lastRegisteredEpoch > maxAge
|
|
79
|
+
) {
|
|
80
|
+
return "agent dir missing and last registration too old";
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Whether orphan-wake pruning may run at all: only when the binds file exists.
|
|
87
|
+
* @param {boolean} bindsFileExists
|
|
88
|
+
*/
|
|
89
|
+
export function mayPruneWakes(bindsFileExists) {
|
|
90
|
+
return bindsFileExists === true;
|
|
91
|
+
}
|
|
92
|
+
|
|
48
93
|
/** @param {string} text */
|
|
49
94
|
export function parseBindsText(text) {
|
|
50
95
|
try {
|
|
@@ -366,22 +411,26 @@ async function main() {
|
|
|
366
411
|
}
|
|
367
412
|
}
|
|
368
413
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
414
|
+
const bindsExist = existsSync(bindsPath);
|
|
415
|
+
if (!mayPruneWakes(bindsExist)) {
|
|
416
|
+
console.log(`no-binds path=${bindsPath} (missing file: pruning nothing)`);
|
|
417
|
+
} else {
|
|
418
|
+
if (entries.length === 0) {
|
|
419
|
+
console.log(`no-binds-empty path=${bindsPath} (pruning orphans)`);
|
|
420
|
+
}
|
|
421
|
+
// Prune orphans — logged one line per stopped process with its reason.
|
|
422
|
+
const snaps = listProcSnapshots();
|
|
423
|
+
for (const s of snaps) {
|
|
424
|
+
if (!shouldPruneWake(s.environ, s.cmdline, binds)) continue;
|
|
425
|
+
const profile = profileFromCmdline(s.cmdline) || "?";
|
|
426
|
+
const agentId = agentIdFromEnviron(s.environ) || "?";
|
|
427
|
+
if (stopWakePid(s.pid)) {
|
|
428
|
+
console.log(
|
|
429
|
+
`stopped orphan pid=${s.pid} profile=${profile} agent_id=${agentId} ts=${new Date().toISOString()} reason=not-in-binds path=${bindsPath}`,
|
|
430
|
+
);
|
|
431
|
+
stopped++;
|
|
432
|
+
}
|
|
380
433
|
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (stopped === 0 && entries.length === 0 && !existsSync(bindsPath)) {
|
|
384
|
-
/* already logged no-binds */
|
|
385
434
|
}
|
|
386
435
|
|
|
387
436
|
process.exit(failed > 0 ? 1 : 0);
|
|
@@ -23,6 +23,15 @@
|
|
|
23
23
|
# AGENTCHAT_NO_PROXY=1
|
|
24
24
|
# unset AGENTCHAT_WAKE_MODE
|
|
25
25
|
#
|
|
26
|
+
# Start non-Grok stacks with the Cursor session env REMOVED — a shell spawned by
|
|
27
|
+
# a Grok agent carries that agent's CURSOR_CONVERSATION_ID, which must not leak
|
|
28
|
+
# into this host (grok-bind would treat it as the Grok bot). e.g. at the top of
|
|
29
|
+
# each start/ensure script (CURSOR_AGENT_STORE_* computed dynamically):
|
|
30
|
+
# u=$(awk 'BEGIN{for(k in ENVIRON) if(k ~ /^CURSOR_AGENT_STORE_/) printf "-u %s ", k}')
|
|
31
|
+
# exec env -u CURSOR_CONVERSATION_ID -u CURSOR_REQUEST_ID -u __CURSOR_SANDBOX_ENV_RESTORE -u CURSOR_AGENT $u sh "$0" "$@"
|
|
32
|
+
# (guard so it only re-execs while one of those keys is still set)
|
|
33
|
+
# Never do this for Grok WAKE_MODE=grok wakes — they need their own id.
|
|
34
|
+
#
|
|
26
35
|
# Suggested start-mcp-wake shape:
|
|
27
36
|
# setsid -f env AGENTCHAT_WAKE_KIND=url \
|
|
28
37
|
# sh -c 'set -a; . wake.env; set +a; unset AGENTCHAT_WAKE_MODE;
|
|
@@ -86,6 +86,27 @@ function sendJson(res, status, obj) {
|
|
|
86
86
|
res.end(body);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Cursor session env that must not leak into a non-Grok host turn (or its
|
|
91
|
+
* AgentsChat MCP child): e.g. a Grok agent's CURSOR_CONVERSATION_ID inherited
|
|
92
|
+
* from the shell that started this receiver. CURSOR_AGENT_STORE_* is dropped too.
|
|
93
|
+
*/
|
|
94
|
+
export const LEAKED_CURSOR_ENV = [
|
|
95
|
+
"CURSOR_CONVERSATION_ID",
|
|
96
|
+
"CURSOR_REQUEST_ID",
|
|
97
|
+
"__CURSOR_SANDBOX_ENV_RESTORE",
|
|
98
|
+
"CURSOR_AGENT",
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
/** Copy of `base` without leaked Cursor session keys. Does not mutate `base`. */
|
|
102
|
+
export function withoutCursorSessionEnv(base) {
|
|
103
|
+
const out = { ...base };
|
|
104
|
+
for (const k of Object.keys(out)) {
|
|
105
|
+
if (LEAKED_CURSOR_ENV.includes(k) || k.startsWith("CURSOR_AGENT_STORE_")) delete out[k];
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
89
110
|
/** Build a prompt string hosts can inject into one dedicated session. */
|
|
90
111
|
export function buildHostPrompt(job) {
|
|
91
112
|
const mentioned = Array.isArray(job.mentioned_ids)
|
|
@@ -217,7 +238,7 @@ function startServer() {
|
|
|
217
238
|
const child = spawn(cmd, {
|
|
218
239
|
shell: true,
|
|
219
240
|
env: {
|
|
220
|
-
...process.env,
|
|
241
|
+
...withoutCursorSessionEnv(process.env),
|
|
221
242
|
AGENTCHAT_URL_WAKE_PROMPT: prompt,
|
|
222
243
|
AGENTCHAT_URL_WAKE_CHANNEL_ID: String(job.channel_id || ""),
|
|
223
244
|
AGENTCHAT_URL_WAKE_MESSAGE_ID: String(job.message_id || ""),
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# grok-bind-register.sh — the ONLY writer of grok-binds.json.
|
|
3
|
+
#
|
|
4
|
+
# grok-bind-register.sh <profile> register the CALLER (own CURSOR_CONVERSATION_ID) as <profile>
|
|
5
|
+
# grok-bind-register.sh --prune drop stale entries (see rules below); logs every prune
|
|
6
|
+
#
|
|
7
|
+
# Each Grok bot registers itself; there is no hard-coded bot list. A machine's
|
|
8
|
+
# binds file therefore only contains the Grok bots actually started there.
|
|
9
|
+
#
|
|
10
|
+
# Register rules:
|
|
11
|
+
# - own uuid = $CURSOR_CONVERSATION_ID, must be a real UUID (sand-subagent-* etc. rejected)
|
|
12
|
+
# - <profile> must be [A-Za-z0-9_-]+ and $AGENTSCHAT_DIR/<profile>.json must exist
|
|
13
|
+
# - flock grok-binds.json.lock; set ONLY own key; write temp + rename; mode 600
|
|
14
|
+
# - sidecar grok-binds.meta.json records {profile, registered_by, ts} per uuid
|
|
15
|
+
# (grok-binds.json itself stays a plain {uuid: profile} map)
|
|
16
|
+
# Prune rules (an entry is removed only if):
|
|
17
|
+
# - its profile file $AGENTSCHAT_DIR/<profile>.json is gone, OR
|
|
18
|
+
# - $AGENT_DATA_DIR/<uuid> is missing AND its last registration (meta ts) is > 7 days old
|
|
19
|
+
# Missing binds file → nothing to prune. Entries without meta ts are never age-pruned.
|
|
20
|
+
# Never prints tokens.
|
|
21
|
+
set -eu
|
|
22
|
+
|
|
23
|
+
AGENTSCHAT_DIR="${AGENTSCHAT_DIR:-${HOME:-/home/box}/.agentschat}"
|
|
24
|
+
BINDS="${AGENTCHAT_GROK_BINDS:-$AGENTSCHAT_DIR/grok-binds.json}"
|
|
25
|
+
META="${BINDS%.json}.meta.json"
|
|
26
|
+
LOCK="$BINDS.lock"
|
|
27
|
+
AGENT_DATA_DIR="${AGENT_DATA_DIR:-${HOME:-/home/box}/agent-data/agents}"
|
|
28
|
+
PRUNE_AGE_DAYS="${GROK_BIND_PRUNE_AGE_DAYS:-7}"
|
|
29
|
+
PRUNE_LOG="${GROK_BIND_PRUNE_LOG:-$AGENTSCHAT_DIR/grok-binds.prune.log}"
|
|
30
|
+
|
|
31
|
+
usage() { echo "usage: $0 <profile> | --prune" >&2; exit 2; }
|
|
32
|
+
[ $# -eq 1 ] || usage
|
|
33
|
+
|
|
34
|
+
mode=register
|
|
35
|
+
profile=""
|
|
36
|
+
case "$1" in
|
|
37
|
+
--prune) mode=prune ;;
|
|
38
|
+
-h|--help) usage ;;
|
|
39
|
+
-*) usage ;;
|
|
40
|
+
*) profile="$1" ;;
|
|
41
|
+
esac
|
|
42
|
+
|
|
43
|
+
uuid=""
|
|
44
|
+
if [ "$mode" = register ]; then
|
|
45
|
+
case "$profile" in
|
|
46
|
+
*[!A-Za-z0-9_-]*|"") echo "grok-bind-register: invalid profile name '$profile'" >&2; exit 2 ;;
|
|
47
|
+
esac
|
|
48
|
+
uuid="${CURSOR_CONVERSATION_ID:-}"
|
|
49
|
+
if ! printf '%s' "$uuid" | grep -Eqi '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'; then
|
|
50
|
+
echo "grok-bind-register: CURSOR_CONVERSATION_ID is not a real agent UUID ('${uuid:-<empty>}'); refusing (subagents must not register)" >&2
|
|
51
|
+
exit 3
|
|
52
|
+
fi
|
|
53
|
+
if [ ! -f "$AGENTSCHAT_DIR/$profile.json" ]; then
|
|
54
|
+
echo "grok-bind-register: missing profile file $AGENTSCHAT_DIR/$profile.json; refusing" >&2
|
|
55
|
+
exit 4
|
|
56
|
+
fi
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
mkdir -p "$(dirname "$BINDS")"
|
|
60
|
+
exec 9>>"$LOCK"
|
|
61
|
+
if ! flock -w 15 9; then
|
|
62
|
+
echo "grok-bind-register: could not lock $LOCK" >&2
|
|
63
|
+
exit 5
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
MODE="$mode" UUID="$uuid" PROFILE="$profile" BINDS="$BINDS" META="$META" \
|
|
67
|
+
AGENTSCHAT_DIR="$AGENTSCHAT_DIR" AGENT_DATA_DIR="$AGENT_DATA_DIR" \
|
|
68
|
+
PRUNE_AGE_DAYS="$PRUNE_AGE_DAYS" PRUNE_LOG="$PRUNE_LOG" python3 - <<'PY'
|
|
69
|
+
import json, os, socket, sys, tempfile, time
|
|
70
|
+
from datetime import datetime, timezone
|
|
71
|
+
|
|
72
|
+
mode = os.environ["MODE"]; binds_p = os.environ["BINDS"]; meta_p = os.environ["META"]
|
|
73
|
+
adir = os.environ["AGENTSCHAT_DIR"]; data_dir = os.environ["AGENT_DATA_DIR"]
|
|
74
|
+
age_days = float(os.environ["PRUNE_AGE_DAYS"]); prune_log = os.environ["PRUNE_LOG"]
|
|
75
|
+
|
|
76
|
+
def load(p):
|
|
77
|
+
try:
|
|
78
|
+
with open(p) as f:
|
|
79
|
+
d = json.load(f)
|
|
80
|
+
return d if isinstance(d, dict) else None
|
|
81
|
+
except FileNotFoundError:
|
|
82
|
+
return {}
|
|
83
|
+
except Exception:
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
def write(p, d):
|
|
87
|
+
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(p) or ".", prefix="." + os.path.basename(p) + ".")
|
|
88
|
+
try:
|
|
89
|
+
with os.fdopen(fd, "w") as f:
|
|
90
|
+
json.dump(d, f, indent=2); f.write("\n")
|
|
91
|
+
os.chmod(tmp, 0o600)
|
|
92
|
+
os.replace(tmp, p)
|
|
93
|
+
except Exception:
|
|
94
|
+
try: os.unlink(tmp)
|
|
95
|
+
except Exception: pass
|
|
96
|
+
raise
|
|
97
|
+
|
|
98
|
+
def now_iso():
|
|
99
|
+
return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")
|
|
100
|
+
|
|
101
|
+
def log_prune(msg):
|
|
102
|
+
line = f"{now_iso()} prune {msg}"
|
|
103
|
+
print(line)
|
|
104
|
+
try:
|
|
105
|
+
with open(prune_log, "a") as f:
|
|
106
|
+
f.write(line + "\n")
|
|
107
|
+
os.chmod(prune_log, 0o600)
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
if mode == "register":
|
|
112
|
+
uuid = os.environ["UUID"]; profile = os.environ["PROFILE"]
|
|
113
|
+
binds = load(binds_p)
|
|
114
|
+
if binds is None:
|
|
115
|
+
print(f"grok-bind-register: {binds_p} is malformed; refusing to overwrite", file=sys.stderr); sys.exit(6)
|
|
116
|
+
meta = load(meta_p) or {}
|
|
117
|
+
prev = binds.get(uuid)
|
|
118
|
+
if prev != profile:
|
|
119
|
+
binds[uuid] = profile
|
|
120
|
+
write(binds_p, binds)
|
|
121
|
+
else:
|
|
122
|
+
os.chmod(binds_p, 0o600)
|
|
123
|
+
meta[uuid] = {"profile": profile,
|
|
124
|
+
"registered_by": f"{uuid}@{socket.gethostname()}",
|
|
125
|
+
"ts": now_iso(), "epoch": int(time.time())}
|
|
126
|
+
write(meta_p, meta)
|
|
127
|
+
action = "refreshed" if prev == profile else (f"rebound(from {prev})" if prev else "registered")
|
|
128
|
+
print(f"grok-bind-register: {action} {uuid} -> {profile}")
|
|
129
|
+
sys.exit(0)
|
|
130
|
+
|
|
131
|
+
# prune
|
|
132
|
+
if not os.path.exists(binds_p):
|
|
133
|
+
print(f"grok-bind-register: no binds file {binds_p}; nothing to prune"); sys.exit(0)
|
|
134
|
+
binds = load(binds_p)
|
|
135
|
+
if binds is None:
|
|
136
|
+
print(f"grok-bind-register: {binds_p} is malformed; not pruning", file=sys.stderr); sys.exit(6)
|
|
137
|
+
meta = load(meta_p) or {}
|
|
138
|
+
now = time.time(); removed = []
|
|
139
|
+
for uuid, profile in list(binds.items()):
|
|
140
|
+
reason = None
|
|
141
|
+
if not isinstance(profile, str) or not profile.strip():
|
|
142
|
+
continue
|
|
143
|
+
if not os.path.exists(os.path.join(adir, f"{profile}.json")):
|
|
144
|
+
reason = f"profile file {profile}.json missing"
|
|
145
|
+
else:
|
|
146
|
+
m = meta.get(uuid) if isinstance(meta.get(uuid), dict) else None
|
|
147
|
+
epoch = m.get("epoch") if m else None
|
|
148
|
+
if not os.path.isdir(os.path.join(data_dir, uuid)) and isinstance(epoch, (int, float)) \
|
|
149
|
+
and now - epoch > age_days * 86400:
|
|
150
|
+
reason = f"agent dir missing and last registration {m.get('ts')} older than {age_days:g}d"
|
|
151
|
+
if reason:
|
|
152
|
+
removed.append(uuid)
|
|
153
|
+
log_prune(f"uuid={uuid} profile={profile} reason={reason}")
|
|
154
|
+
if removed:
|
|
155
|
+
for u in removed:
|
|
156
|
+
binds.pop(u, None); meta.pop(u, None)
|
|
157
|
+
write(binds_p, binds); write(meta_p, meta)
|
|
158
|
+
print(f"grok-bind-register: prune done removed={len(removed)} kept={len(binds)}")
|
|
159
|
+
PY
|