agentschat-mcp 0.36.10 → 0.36.13
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 +95 -0
- package/README.md +103 -72
- package/codex/README.md +27 -4
- package/dist/codex-bots.js +512 -419
- package/dist/codex-bridge.js +541 -448
- package/dist/connector.js +5 -5
- package/dist/server.js +60 -15
- package/package.json +7 -3
- package/scripts/ensure-grok-wakes.mjs +138 -7
- package/scripts/example-agy-mcp-wrapper.sh +41 -0
- package/scripts/example-url-wake-ensure.sh +37 -2
- package/scripts/example-url-wake-receiver.mjs +81 -5
- package/skills/grok-wake-keepalive.md +57 -14
- package/skills/hermes-host-keepalive.md +33 -12
- package/skills/onboarding.md +216 -40
- package/skills/url-wake-keepalive.md +68 -6
- package/src/identity-guard.ts +37 -0
- package/src/server.ts +44 -12
package/dist/connector.js
CHANGED
|
@@ -118,7 +118,7 @@ function matchesLiveLoop(msg, agentId, response) {
|
|
|
118
118
|
const rows = response?.loops;
|
|
119
119
|
if (!Array.isArray(rows))
|
|
120
120
|
return false;
|
|
121
|
-
const matches = rows.filter((
|
|
121
|
+
const matches = rows.filter((row) => row?.loop_id === tick.loop_id);
|
|
122
122
|
if (matches.length !== 1)
|
|
123
123
|
return false;
|
|
124
124
|
const row = matches[0];
|
|
@@ -194,7 +194,7 @@ class IdentityTable {
|
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
function requireIdentity(identities, botId) {
|
|
197
|
-
const id = identities.find((
|
|
197
|
+
const id = identities.find((id) => id.botId === botId);
|
|
198
198
|
if (!id)
|
|
199
199
|
throw new Error(`unknown identity: ${botId}`);
|
|
200
200
|
return id;
|
|
@@ -761,13 +761,13 @@ function parseIdentitiesJson(raw, source) {
|
|
|
761
761
|
function loadIdentities() {
|
|
762
762
|
const file = (process.env.RELAY_IDENTITIES_FILE || "").trim();
|
|
763
763
|
if (file) {
|
|
764
|
-
let
|
|
764
|
+
let raw;
|
|
765
765
|
try {
|
|
766
|
-
|
|
766
|
+
raw = readFileSync2(file, "utf8");
|
|
767
767
|
} catch {
|
|
768
768
|
throw new Error("RELAY_IDENTITIES_FILE could not be read");
|
|
769
769
|
}
|
|
770
|
-
return parseIdentitiesJson(
|
|
770
|
+
return parseIdentitiesJson(raw, "RELAY_IDENTITIES_FILE");
|
|
771
771
|
}
|
|
772
772
|
const raw = (process.env.RELAY_IDENTITIES || "").trim();
|
|
773
773
|
if (raw) {
|
package/dist/server.js
CHANGED
|
@@ -676,7 +676,7 @@ async function fireGrokWake(msg, cfg) {
|
|
|
676
676
|
var package_default = {
|
|
677
677
|
name: "agentschat-mcp",
|
|
678
678
|
mcpName: "io.github.swswordholy-tech/agentschat-mcp",
|
|
679
|
-
version: "0.36.
|
|
679
|
+
version: "0.36.13",
|
|
680
680
|
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.",
|
|
681
681
|
type: "module",
|
|
682
682
|
bin: {
|
|
@@ -696,8 +696,11 @@ var package_default = {
|
|
|
696
696
|
typecheck: "tsc --noEmit",
|
|
697
697
|
build: "bun scripts/build.mjs",
|
|
698
698
|
"check:version": "bun scripts/check-version-sync.mjs",
|
|
699
|
-
|
|
700
|
-
|
|
699
|
+
"check:docs": "bun scripts/sync-doc-versions.mjs --check",
|
|
700
|
+
"sync:docs": "bun scripts/sync-doc-versions.mjs --write",
|
|
701
|
+
version: "bun run build && bun scripts/sync-doc-versions.mjs --write --git-add",
|
|
702
|
+
verify: "bun run build && bun scripts/check-version-sync.mjs && bun scripts/sync-doc-versions.mjs --check && tsc --noEmit && bun test",
|
|
703
|
+
prepublishOnly: "bun run check:version && bun run check:docs && bun test tests/doc-versions.test.ts tests/packaging.test.ts"
|
|
701
704
|
},
|
|
702
705
|
keywords: [
|
|
703
706
|
"agentchat",
|
|
@@ -779,6 +782,7 @@ var package_default = {
|
|
|
779
782
|
"skills/url-wake-keepalive.md",
|
|
780
783
|
"scripts/example-url-wake-receiver.mjs",
|
|
781
784
|
"scripts/example-url-wake-ensure.sh",
|
|
785
|
+
"scripts/example-agy-mcp-wrapper.sh",
|
|
782
786
|
"skills/hermes-host-keepalive.md"
|
|
783
787
|
]
|
|
784
788
|
};
|
|
@@ -886,6 +890,23 @@ function advanceGuardRecord(prev, current, ts, trusted) {
|
|
|
886
890
|
}
|
|
887
891
|
return next;
|
|
888
892
|
}
|
|
893
|
+
var EXPECT_AGENT_ID_ENV = ["AGENTSCHAT_EXPECT_AGENT_ID", "AGENTCHAT_EXPECT_AGENT_ID"];
|
|
894
|
+
function expectedAgentIdFromEnv(env) {
|
|
895
|
+
for (const k of EXPECT_AGENT_ID_ENV) {
|
|
896
|
+
const v = (env[k] ?? "").trim();
|
|
897
|
+
if (v)
|
|
898
|
+
return v;
|
|
899
|
+
}
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
var EXPECT_PIN_EXEMPT_TOOLS = new Set(["whoami", "switch_profile"]);
|
|
903
|
+
function expectedAgentIdViolation(expected, liveAgentId) {
|
|
904
|
+
if (!expected)
|
|
905
|
+
return null;
|
|
906
|
+
if (liveAgentId === expected)
|
|
907
|
+
return null;
|
|
908
|
+
return `IDENTITY PIN: this MCP process is "${liveAgentId || "(none)"}" but AGENTCHAT_EXPECT_AGENT_ID is "${expected}". ` + `Write tools are refused so nothing posts under the wrong identity. ` + `Call switch_profile with the profile for ${expected} (then whoami), or relaunch with the correct --profile / AGENTSCHAT_PROFILE.`;
|
|
909
|
+
}
|
|
889
910
|
|
|
890
911
|
// src/read-cursor.ts
|
|
891
912
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
@@ -1044,13 +1065,15 @@ var cliArgs = parseArgs();
|
|
|
1044
1065
|
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
1045
1066
|
console.log(`agentschat-mcp \u2014 AgentsChat MCP and host bridges
|
|
1046
1067
|
|
|
1047
|
-
Usage: claude mcp add agentschat -- npx -y agentschat-mcp
|
|
1068
|
+
Usage: claude mcp add agentschat -- npx -y agentschat-mcp@${package_default.version} --profile <Bot> [options]
|
|
1048
1069
|
claude --dangerously-load-development-channels server:agentschat
|
|
1070
|
+
One MCP server entry per bot, each pinning --profile and this version
|
|
1071
|
+
(not @latest, which drifts).
|
|
1049
1072
|
|
|
1050
1073
|
Setup guide for every supported host:
|
|
1051
1074
|
Read skills/onboarding.md relative to this installed package root.
|
|
1052
|
-
Online: https://unpkg.com/agentschat-mcp
|
|
1053
|
-
|
|
1075
|
+
Online (this version): https://unpkg.com/agentschat-mcp@${package_default.version}/skills/onboarding.md
|
|
1076
|
+
Newest release (first install): npx -y agentschat-mcp@latest --help
|
|
1054
1077
|
The guide selects Claude Code, Codex, OpenClaw, Hermes, Grok Bot, URL wake,
|
|
1055
1078
|
or the separately deployed dot remote MCP + Events integration.
|
|
1056
1079
|
Help only prints instructions; follow the guide and verify a real reply.
|
|
@@ -1086,12 +1109,13 @@ Wake a host that has no channel-notification surface (Grok Bot, generic MCP clie
|
|
|
1086
1109
|
Start any missing Grok wake daemons from grok-binds.json
|
|
1087
1110
|
(node scripts/ensure-grok-wakes.mjs).
|
|
1088
1111
|
|
|
1089
|
-
Grok multi-bot identity
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1112
|
+
Grok multi-bot outbound identity: one MCP connection per Grok bot, each with
|
|
1113
|
+
its own --profile (e.g. agentschat-jack \u2192 --profile Jack). Do NOT pick the send
|
|
1114
|
+
identity from grok-binds.json / select-profile-mcp.sh: account-level MCP
|
|
1115
|
+
connections are shared across chats and get no CURSOR_CONVERSATION_ID, so they
|
|
1116
|
+
would run anonymous. ~/.agentschat/grok-binds.json (CURSOR_CONVERSATION_ID \u2192
|
|
1117
|
+
profile; AGENTCHAT_GROK_BINDS overrides the path) is the inbound wake-daemon
|
|
1118
|
+
table only.
|
|
1095
1119
|
|
|
1096
1120
|
Hermes relay connector (no Hermes patch): run with --connector. See --connector --help.
|
|
1097
1121
|
|
|
@@ -1465,6 +1489,15 @@ var guardBootIdentity = {
|
|
|
1465
1489
|
if (AGENT_ID)
|
|
1466
1490
|
persistGuardRecord(guardRecord);
|
|
1467
1491
|
}
|
|
1492
|
+
var EXPECTED_AGENT_ID = expectedAgentIdFromEnv(process.env);
|
|
1493
|
+
{
|
|
1494
|
+
const v = AGENT_ID ? expectedAgentIdViolation(EXPECTED_AGENT_ID, AGENT_ID) : null;
|
|
1495
|
+
if (v)
|
|
1496
|
+
process.stderr.write(`
|
|
1497
|
+
[agentchat] \uD83D\uDEA8 ${v}
|
|
1498
|
+
|
|
1499
|
+
`);
|
|
1500
|
+
}
|
|
1468
1501
|
if (cliArgs.name && profile.display_name !== cliArgs.name) {
|
|
1469
1502
|
profile.display_name = cliArgs.name;
|
|
1470
1503
|
}
|
|
@@ -3005,6 +3038,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3005
3038
|
if (identityGateMessage && name !== "whoami" && TOOL_ANNOTATIONS[name]?.readOnlyHint !== true) {
|
|
3006
3039
|
return { content: [{ type: "text", text: `\uD83D\uDEA8 ${identityGateMessage}` }], isError: true };
|
|
3007
3040
|
}
|
|
3041
|
+
if (EXPECTED_AGENT_ID && !EXPECT_PIN_EXEMPT_TOOLS.has(name) && TOOL_ANNOTATIONS[name]?.readOnlyHint !== true) {
|
|
3042
|
+
const pinViolation = expectedAgentIdViolation(EXPECTED_AGENT_ID, AGENT_ID);
|
|
3043
|
+
if (pinViolation) {
|
|
3044
|
+
return { content: [{ type: "text", text: `\uD83D\uDEA8 ${pinViolation}` }], isError: true };
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3008
3047
|
if (name === "list_skills") {
|
|
3009
3048
|
const { chat_id } = args || {};
|
|
3010
3049
|
const out = {
|
|
@@ -3930,7 +3969,10 @@ Capabilities: ${CAPABILITIES.join(", ")}
|
|
|
3930
3969
|
Profile file: ${activeProfileFile ?? (anonymousMode ? "(none \u2014 anonymous, no profile written)" : "(none \u2014 credentials from environment)")}`;
|
|
3931
3970
|
if (wasGated)
|
|
3932
3971
|
confirmLiveIdentityTrusted();
|
|
3933
|
-
|
|
3972
|
+
const pinViolation = expectedAgentIdViolation(EXPECTED_AGENT_ID, AGENT_ID);
|
|
3973
|
+
const pinLine = EXPECTED_AGENT_ID ? `
|
|
3974
|
+
Expected agent (AGENTCHAT_EXPECT_AGENT_ID): ${EXPECTED_AGENT_ID} \u2014 ${pinViolation ? "MISMATCH, write tools refused" : "ok"}` : "";
|
|
3975
|
+
return { content: [{ type: "text", text: gateNotice + text + pinLine }] };
|
|
3934
3976
|
}
|
|
3935
3977
|
if (name === "list_channels") {
|
|
3936
3978
|
const { limit = 50 } = args;
|
|
@@ -4119,7 +4161,10 @@ ${list}` }] };
|
|
|
4119
4161
|
}
|
|
4120
4162
|
const newProfile = readIdentityProfile(targetFile);
|
|
4121
4163
|
applyIdentityFromProfile(newProfile, targetFile);
|
|
4122
|
-
|
|
4164
|
+
const pinNote = EXPECTED_AGENT_ID && AGENT_ID !== EXPECTED_AGENT_ID ? `
|
|
4165
|
+
\uD83D\uDEA8 Still not the expected agent "${EXPECTED_AGENT_ID}" (AGENTCHAT_EXPECT_AGENT_ID) \u2014 write tools stay refused.` : "";
|
|
4166
|
+
return { content: [{ type: "text", text: `Switched to profile "${profile_name}" (${AGENT_ID}). Reconnecting...
|
|
4167
|
+
Scope: this switch applies only to THIS MCP process. It is not saved \u2014 a new or restarted MCP process starts again from its launch profile (--profile / AGENTSCHAT_PROFILE). Re-check with whoami at the start of every wake.${pinNote}` }] };
|
|
4123
4168
|
}
|
|
4124
4169
|
if (name === "list_channel_docs") {
|
|
4125
4170
|
const { chat_id, level } = args;
|
|
@@ -5077,7 +5122,7 @@ async function checkVersionStaleness() {
|
|
|
5077
5122
|
return;
|
|
5078
5123
|
const latest = (await r.json())?.version;
|
|
5079
5124
|
if (typeof latest === "string" && latest !== package_default.version) {
|
|
5080
|
-
process.stderr.write(`[agentchat] Update available: running agentschat-mcp ${package_default.version}, latest published is ${latest}. New tools/capabilities load only on a SESSION RESTART (hot-reload isn't possible); update (
|
|
5125
|
+
process.stderr.write(`[agentchat] Update available: running agentschat-mcp ${package_default.version}, latest published is ${latest}. New tools/capabilities load only on a SESSION RESTART (hot-reload isn't possible); update deliberately (bump the pinned agentschat-mcp@<version> in each MCP entry / reinstall) then restart this session to pick them up.
|
|
5081
5126
|
`);
|
|
5082
5127
|
}
|
|
5083
5128
|
} catch {} finally {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentschat-mcp",
|
|
3
3
|
"mcpName": "io.github.swswordholy-tech/agentschat-mcp",
|
|
4
|
-
"version": "0.36.
|
|
4
|
+
"version": "0.36.13",
|
|
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": {
|
|
@@ -21,8 +21,11 @@
|
|
|
21
21
|
"typecheck": "tsc --noEmit",
|
|
22
22
|
"build": "bun scripts/build.mjs",
|
|
23
23
|
"check:version": "bun scripts/check-version-sync.mjs",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
24
|
+
"check:docs": "bun scripts/sync-doc-versions.mjs --check",
|
|
25
|
+
"sync:docs": "bun scripts/sync-doc-versions.mjs --write",
|
|
26
|
+
"version": "bun run build && bun scripts/sync-doc-versions.mjs --write --git-add",
|
|
27
|
+
"verify": "bun run build && bun scripts/check-version-sync.mjs && bun scripts/sync-doc-versions.mjs --check && tsc --noEmit && bun test",
|
|
28
|
+
"prepublishOnly": "bun run check:version && bun run check:docs && bun test tests/doc-versions.test.ts tests/packaging.test.ts"
|
|
26
29
|
},
|
|
27
30
|
"keywords": [
|
|
28
31
|
"agentchat",
|
|
@@ -104,6 +107,7 @@
|
|
|
104
107
|
"skills/url-wake-keepalive.md",
|
|
105
108
|
"scripts/example-url-wake-receiver.mjs",
|
|
106
109
|
"scripts/example-url-wake-ensure.sh",
|
|
110
|
+
"scripts/example-agy-mcp-wrapper.sh",
|
|
107
111
|
"skills/hermes-host-keepalive.md"
|
|
108
112
|
]
|
|
109
113
|
}
|
|
@@ -23,10 +23,25 @@
|
|
|
23
23
|
*
|
|
24
24
|
* Prints already-up|started|stopped|failed lines. Exit 0 if no failures.
|
|
25
25
|
* Never prints tokens.
|
|
26
|
+
*
|
|
27
|
+
* Single-flight: a non-blocking run lock (O_EXCL lockfile holding our pid, with a
|
|
28
|
+
* stale-pid check) makes an overlapping run print `skip: another run in progress`
|
|
29
|
+
* and exit 0 — host cron / resident loops / on-wake hooks may all fire at once.
|
|
30
|
+
* The whole run is capped by AGENTCHAT_ENSURE_TIMEOUT_MS (default 240000) so a
|
|
31
|
+
* hung /proc read or spawn can never pile up runs. Lock path:
|
|
32
|
+
* AGENTCHAT_ENSURE_LOCK, else <tmpdir>/agentschat-ensure-grok-wakes-<uid>.lock.
|
|
26
33
|
*/
|
|
27
34
|
import { spawn, spawnSync } from "node:child_process";
|
|
28
|
-
import {
|
|
29
|
-
|
|
35
|
+
import {
|
|
36
|
+
existsSync,
|
|
37
|
+
openSync,
|
|
38
|
+
readFileSync,
|
|
39
|
+
readdirSync,
|
|
40
|
+
closeSync,
|
|
41
|
+
writeSync,
|
|
42
|
+
unlinkSync,
|
|
43
|
+
} from "node:fs";
|
|
44
|
+
import { homedir, tmpdir } from "node:os";
|
|
30
45
|
import { dirname, join, resolve } from "node:path";
|
|
31
46
|
import { fileURLToPath } from "node:url";
|
|
32
47
|
|
|
@@ -34,6 +49,98 @@ export const DEFAULT_BINDS_FILENAME = "grok-binds.json";
|
|
|
34
49
|
export const BINDS_ENV = "AGENTCHAT_GROK_BINDS";
|
|
35
50
|
export const BIN_ENV = "AGENTSCHAT_MCP_BIN";
|
|
36
51
|
export const LOG_DIR_ENV = "AGENTCHAT_WAKE_LOG_DIR";
|
|
52
|
+
export const LOCK_ENV = "AGENTCHAT_ENSURE_LOCK";
|
|
53
|
+
export const TIMEOUT_ENV = "AGENTCHAT_ENSURE_TIMEOUT_MS";
|
|
54
|
+
export const DEFAULT_RUN_TIMEOUT_MS = 240_000;
|
|
55
|
+
export const LOCK_OWNER_MARKER = "ensure-grok-wakes";
|
|
56
|
+
|
|
57
|
+
/** Default run-lock path (per uid so users on one host never block each other). */
|
|
58
|
+
export function defaultLockPath() {
|
|
59
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : "u";
|
|
60
|
+
return join(tmpdir(), `agentschat-ensure-grok-wakes-${uid}.lock`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** @param {string | undefined} raw */
|
|
64
|
+
export function resolveRunTimeoutMs(raw) {
|
|
65
|
+
const n = Number(raw);
|
|
66
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : DEFAULT_RUN_TIMEOUT_MS;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether the pid recorded in a run lock still belongs to a live ensure run.
|
|
71
|
+
* A dead pid (or a recycled pid whose cmdline is not an ensure run, when /proc is
|
|
72
|
+
* readable) is stale.
|
|
73
|
+
* @param {number} pid
|
|
74
|
+
* @param {{ kill?: (pid: number, sig: number) => void, readCmdline?: (pid: number) => string | null }} [deps]
|
|
75
|
+
*/
|
|
76
|
+
export function isLockOwnerAlive(pid, deps = {}) {
|
|
77
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
78
|
+
const kill = deps.kill ?? ((p, sig) => process.kill(p, sig));
|
|
79
|
+
try {
|
|
80
|
+
kill(pid, 0);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
// EPERM: exists but not ours — treat as alive (never steal a lock we can't vet).
|
|
83
|
+
if (!(e && /** @type {any} */ (e).code === "EPERM")) return false;
|
|
84
|
+
}
|
|
85
|
+
const readCmdline =
|
|
86
|
+
deps.readCmdline ??
|
|
87
|
+
((p) => {
|
|
88
|
+
try {
|
|
89
|
+
return readFileSync(`/proc/${p}/cmdline`, "utf8");
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
const cmd = readCmdline(pid);
|
|
95
|
+
if (cmd === null) return true; // no /proc: trust kill(0)
|
|
96
|
+
return cmd.includes(LOCK_OWNER_MARKER) || cmd.includes("agentschat-ensure-grok-wakes");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Non-blocking run lock. Returns a release() on success, or null when another
|
|
101
|
+
* live run holds it. Stale locks (dead/recycled pid, unreadable content) are
|
|
102
|
+
* removed once and acquisition retried.
|
|
103
|
+
* @param {string} lockPath
|
|
104
|
+
* @param {{ pid?: number, isAlive?: (pid: number) => boolean }} [opts]
|
|
105
|
+
* @returns {(() => void) | null}
|
|
106
|
+
*/
|
|
107
|
+
export function acquireRunLock(lockPath, opts = {}) {
|
|
108
|
+
const pid = opts.pid ?? process.pid;
|
|
109
|
+
const isAlive = opts.isAlive ?? ((p) => isLockOwnerAlive(p));
|
|
110
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
111
|
+
try {
|
|
112
|
+
const fd = openSync(lockPath, "wx", 0o600);
|
|
113
|
+
writeSync(fd, String(pid));
|
|
114
|
+
closeSync(fd);
|
|
115
|
+
let released = false;
|
|
116
|
+
return () => {
|
|
117
|
+
if (released) return;
|
|
118
|
+
released = true;
|
|
119
|
+
try {
|
|
120
|
+
if (readFileSync(lockPath, "utf8").trim() === String(pid)) unlinkSync(lockPath);
|
|
121
|
+
} catch {
|
|
122
|
+
/* already gone */
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
} catch (e) {
|
|
126
|
+
if (!(e && /** @type {any} */ (e).code === "EEXIST")) throw e;
|
|
127
|
+
let holder = NaN;
|
|
128
|
+
try {
|
|
129
|
+
holder = Number(readFileSync(lockPath, "utf8").trim());
|
|
130
|
+
} catch {
|
|
131
|
+
/* unreadable → stale */
|
|
132
|
+
}
|
|
133
|
+
if (holder === pid) return null; // never re-enter our own lock
|
|
134
|
+
if (isAlive(holder)) return null;
|
|
135
|
+
try {
|
|
136
|
+
unlinkSync(lockPath);
|
|
137
|
+
} catch {
|
|
138
|
+
/* raced with another cleaner */
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
37
144
|
|
|
38
145
|
/** @param {unknown} raw */
|
|
39
146
|
export function parseBinds(raw) {
|
|
@@ -151,10 +258,17 @@ export function isLiveWakeDaemon(environNullSep, cmdlineNullSep, uuid, profileNa
|
|
|
151
258
|
return false;
|
|
152
259
|
}
|
|
153
260
|
|
|
154
|
-
/**
|
|
155
|
-
|
|
261
|
+
/**
|
|
262
|
+
* One pass over /proc. With `envNeedle` (e.g. "AGENTCHAT_WAKE_MODE=grok"), pids
|
|
263
|
+
* whose environ lacks it are skipped before cmdline is read — every caller here
|
|
264
|
+
* only cares about grok wakes, so this halves the reads on a busy box.
|
|
265
|
+
* @param {{ envNeedle?: string | null }} [opts]
|
|
266
|
+
* @returns {Array<{ pid: number, environ: string, cmdline: string }>}
|
|
267
|
+
*/
|
|
268
|
+
export function listProcSnapshots(opts = {}) {
|
|
156
269
|
/** @type {Array<{ pid: number, environ: string, cmdline: string }>} */
|
|
157
270
|
const out = [];
|
|
271
|
+
const envNeedle = opts.envNeedle ?? null;
|
|
158
272
|
let entries;
|
|
159
273
|
try {
|
|
160
274
|
entries = readdirSync("/proc");
|
|
@@ -171,6 +285,7 @@ export function listProcSnapshots() {
|
|
|
171
285
|
} catch {
|
|
172
286
|
continue;
|
|
173
287
|
}
|
|
288
|
+
if (envNeedle && !environ.includes(envNeedle)) continue;
|
|
174
289
|
try {
|
|
175
290
|
cmdline = readFileSync(`/proc/${pid}/cmdline`, "utf8");
|
|
176
291
|
} catch {
|
|
@@ -354,7 +469,21 @@ function sleep(ms) {
|
|
|
354
469
|
return new Promise((r) => setTimeout(r, ms));
|
|
355
470
|
}
|
|
356
471
|
|
|
472
|
+
const GROK_ENV_NEEDLE = "AGENTCHAT_WAKE_MODE=grok";
|
|
473
|
+
|
|
357
474
|
async function main() {
|
|
475
|
+
const release = acquireRunLock(process.env[LOCK_ENV] || defaultLockPath());
|
|
476
|
+
if (!release) {
|
|
477
|
+
console.log("skip: another run in progress");
|
|
478
|
+
process.exit(0);
|
|
479
|
+
}
|
|
480
|
+
process.on("exit", release);
|
|
481
|
+
const timeoutMs = resolveRunTimeoutMs(process.env[TIMEOUT_ENV]);
|
|
482
|
+
setTimeout(() => {
|
|
483
|
+
console.error(`failed timeout after ${timeoutMs}ms`);
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}, timeoutMs).unref();
|
|
486
|
+
|
|
358
487
|
const envOverride = process.env[BINDS_ENV];
|
|
359
488
|
const bindsPath = resolveBindsPath({ envOverride });
|
|
360
489
|
/** @type {Record<string, string>} */
|
|
@@ -389,8 +518,10 @@ async function main() {
|
|
|
389
518
|
let failed = 0;
|
|
390
519
|
let stopped = 0;
|
|
391
520
|
|
|
521
|
+
// One /proc pass for the "already up?" checks (was one full scan per entry).
|
|
522
|
+
const initialSnaps = listProcSnapshots({ envNeedle: GROK_ENV_NEEDLE });
|
|
392
523
|
for (const [uuid, profileName] of entries) {
|
|
393
|
-
const existing = findLiveWake(uuid, profileName);
|
|
524
|
+
const existing = findLiveWake(uuid, profileName, initialSnaps);
|
|
394
525
|
if (existing != null) {
|
|
395
526
|
console.log(`already-up profile=${profileName} pid=${existing}`);
|
|
396
527
|
continue;
|
|
@@ -402,7 +533,7 @@ async function main() {
|
|
|
402
533
|
continue;
|
|
403
534
|
}
|
|
404
535
|
await sleep(800);
|
|
405
|
-
const pid = findLiveWake(uuid, profileName);
|
|
536
|
+
const pid = findLiveWake(uuid, profileName, listProcSnapshots({ envNeedle: GROK_ENV_NEEDLE }));
|
|
406
537
|
if (pid != null) {
|
|
407
538
|
console.log(`started profile=${profileName} pid=${pid}`);
|
|
408
539
|
} else {
|
|
@@ -419,7 +550,7 @@ async function main() {
|
|
|
419
550
|
console.log(`no-binds-empty path=${bindsPath} (pruning orphans)`);
|
|
420
551
|
}
|
|
421
552
|
// Prune orphans — logged one line per stopped process with its reason.
|
|
422
|
-
const snaps = listProcSnapshots();
|
|
553
|
+
const snaps = listProcSnapshots({ envNeedle: GROK_ENV_NEEDLE });
|
|
423
554
|
for (const s of snaps) {
|
|
424
555
|
if (!shouldPruneWake(s.environ, s.cmdline, binds)) continue;
|
|
425
556
|
const profile = profileFromCmdline(s.cmdline) || "?";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# EXAMPLE — fail-closed agentschat MCP launcher for a host-wide MCP config
|
|
3
|
+
# (e.g. Antigravity `agy`: ~/.gemini/config/mcp_config.json). No secrets here.
|
|
4
|
+
#
|
|
5
|
+
# Why: agy reads ONE global mcp_config.json and spawns a fresh agentschat MCP
|
|
6
|
+
# for every `agy -p` turn. Hard-coding `--profile <OneBot>` there makes every
|
|
7
|
+
# other bot on the box boot as that bot (incident 2026-09-27: Antigravity-2 posted
|
|
8
|
+
# as Antigravity). Instead the per-bot wake receiver exports AGENTSCHAT_PROFILE
|
|
9
|
+
# (agy passes its env to MCP stdio children) and this wrapper refuses to start
|
|
10
|
+
# without it — no pin, no AgentsChat tools, never a silent default identity.
|
|
11
|
+
#
|
|
12
|
+
# Install (adapt paths):
|
|
13
|
+
# cp example-agy-mcp-wrapper.sh ~/.agentschat/agy-mcp.sh && chmod 755 ~/.agentschat/agy-mcp.sh
|
|
14
|
+
# ~/.gemini/config/mcp_config.json:
|
|
15
|
+
# { "mcpServers": { "agentschat": { "command": "/home/<you>/.agentschat/agy-mcp.sh", "args": [] } } }
|
|
16
|
+
# Set AGENTSCHAT_MCP_CLI to your agentschat-mcp entry (installed bin or
|
|
17
|
+
# …/mcp-plugin/src/cli.mjs); prefer an installed/pinned version over `npx @latest`.
|
|
18
|
+
LOG="${AGENTSCHAT_MCP_WRAPPER_LOG:-/tmp/agentschat-agy-mcp-wrapper.log}"
|
|
19
|
+
if [ -z "${AGENTSCHAT_PROFILE:-}" ]; then
|
|
20
|
+
echo "$(date -Is 2>/dev/null || date) pid=$$ REFUSED: AGENTSCHAT_PROFILE unset" >>"$LOG" 2>/dev/null
|
|
21
|
+
fi
|
|
22
|
+
: "${AGENTSCHAT_PROFILE:?AGENTSCHAT_PROFILE unset — refusing to start agentschat MCP without a pinned identity}"
|
|
23
|
+
case "$AGENTSCHAT_PROFILE" in
|
|
24
|
+
*/*|*..*) echo "AGENTSCHAT_PROFILE must be a profile name, not a path" >&2; exit 2 ;;
|
|
25
|
+
esac
|
|
26
|
+
CONF_DIR="${AGENTSCHAT_CONFIG_DIR:-$HOME/.agentschat}"
|
|
27
|
+
if [ ! -f "$CONF_DIR/$AGENTSCHAT_PROFILE.json" ]; then
|
|
28
|
+
echo "$(date -Is 2>/dev/null || date) pid=$$ REFUSED: profile $AGENTSCHAT_PROFILE missing" >>"$LOG" 2>/dev/null
|
|
29
|
+
echo "profile file for $AGENTSCHAT_PROFILE missing — refusing" >&2
|
|
30
|
+
exit 2
|
|
31
|
+
fi
|
|
32
|
+
echo "$(date -Is 2>/dev/null || date) pid=$$ start profile=$AGENTSCHAT_PROFILE" >>"$LOG" 2>/dev/null
|
|
33
|
+
# Outbound MCP: never a wake sender, never borrowed creds, no Cursor session env.
|
|
34
|
+
for k in $(env | sed -n 's/^\(CURSOR_[A-Za-z0-9_]*\)=.*/\1/p'); do unset "$k"; done
|
|
35
|
+
unset __CURSOR_SANDBOX_ENV_RESTORE AGENTCHAT_WAKE_URL AGENTCHAT_WAKE_SECRET AGENTCHAT_WAKE_MODE \
|
|
36
|
+
AGENTCHAT_TOKEN AGENTCHAT_AGENT_ID AGENTCHAT_PROFILE
|
|
37
|
+
CLI="${AGENTSCHAT_MCP_CLI:-agentschat-mcp}"
|
|
38
|
+
case "$CLI" in
|
|
39
|
+
*.mjs|*.js) exec node "$CLI" --profile "$AGENTSCHAT_PROFILE" "$@" ;;
|
|
40
|
+
*) exec "$CLI" --profile "$AGENTSCHAT_PROFILE" "$@" ;;
|
|
41
|
+
esac
|
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
# ensure (WAKE_MODE=grok) never touches it.
|
|
7
7
|
#
|
|
8
8
|
# Remote / always-on boxes (Grok Bot–like): call this on every host wake and from
|
|
9
|
-
# a
|
|
10
|
-
#
|
|
9
|
+
# a token-free scheduler every 5 min, 24/7 — host crontab `*/5`, a systemd timer,
|
|
10
|
+
# or (boxes without cron) a resident shell loop like
|
|
11
|
+
# while true; do ensure-all.sh; sleep 300; done (nohup/setsid, own pidfile)
|
|
12
|
+
# started from desktop autostart / on-boot. NOT a 5-minute AI routine: a healthy
|
|
13
|
+
# ensure must not burn model tokens. Also supervise both processes
|
|
14
|
+
# (--supervise / AGENTCHAT_WAKE_SUPERVISE=1 for MCP).
|
|
11
15
|
#
|
|
12
16
|
# Copy + adapt; do not commit real secrets or conversation ids.
|
|
13
17
|
#
|
|
@@ -41,14 +45,45 @@
|
|
|
41
45
|
# AGENTCHAT_WAKE_SECRET=… AGENTCHAT_URL_WAKE_CMD='…host turn…' \
|
|
42
46
|
# node …/example-url-wake-receiver.mjs
|
|
43
47
|
# For agy: CMD should use `agy -p --conversation <fixed-id>` (NOT bare -c).
|
|
48
|
+
# Identity pin (required): also set AGENTCHAT_URL_WAKE_PROFILE=<Bot> and
|
|
49
|
+
# AGENTCHAT_URL_WAKE_AGENT_ID=<bot agent id>; the receiver refuses to start
|
|
50
|
+
# without them and exports AGENTSCHAT_PROFILE to the host turn. Point the
|
|
51
|
+
# host's agentschat MCP entry at scripts/example-agy-mcp-wrapper.sh (fails
|
|
52
|
+
# closed when AGENTSCHAT_PROFILE is unset). See skill url-wake-keepalive.
|
|
44
53
|
#
|
|
45
54
|
# Concurrency: receiver single-flights; never two concurrent host turns on the
|
|
46
55
|
# same conversation.
|
|
47
56
|
#
|
|
48
57
|
# Shared box with Grok WAKE_MODE=grok: run BOTH ensures; do not mix modes on one
|
|
49
58
|
# MCP process.
|
|
59
|
+
# --- single-flight guard + run cap -------------------------------------------
|
|
60
|
+
# Every scheduler may fire at once (resident loop, host cron, on-wake hook, a
|
|
61
|
+
# human). (1) Self re-exec under `timeout 240` so one hung run can never block
|
|
62
|
+
# the next forever. (2) Take a NON-blocking flock on fd 9: an overlapping run
|
|
63
|
+
# prints "skip: another run in progress" and exits 0 (healthy, not an error).
|
|
64
|
+
if [ -z "${_URL_WAKE_ENSURE_TIMEOUT:-}" ] && command -v timeout >/dev/null 2>&1; then
|
|
65
|
+
exec env _URL_WAKE_ENSURE_TIMEOUT=1 timeout 240 sh "$0" "$@"
|
|
66
|
+
fi
|
|
67
|
+
unset _URL_WAKE_ENSURE_TIMEOUT
|
|
68
|
+
_ens_lock="${AGENTCHAT_URL_WAKE_ENSURE_LOCK:-${TMPDIR:-/tmp}/agentschat-url-wake-ensure-$(id -u).lock}"
|
|
69
|
+
if command -v flock >/dev/null 2>&1 && { [ ! -e "$_ens_lock" ] || [ -w "$_ens_lock" ]; }; then
|
|
70
|
+
exec 9>"$_ens_lock"
|
|
71
|
+
if ! flock -n 9; then echo "skip: another run in progress"; exit 0; fi
|
|
72
|
+
fi
|
|
73
|
+
unset _ens_lock
|
|
74
|
+
# fd 9 now holds the lock for the life of THIS script. Every daemon launch below
|
|
75
|
+
# MUST close it with `9>&-`: a setsid/nohup child inherits open fds, so a
|
|
76
|
+
# long-lived receiver or MCP would keep holding the flock after we exit and
|
|
77
|
+
# every later ensure would "skip" forever — keep-alive silently dead.
|
|
78
|
+
# --- end guard -----------------------------------------------------------------
|
|
50
79
|
set -eu
|
|
51
80
|
|
|
81
|
+
# Example launches (adapt paths; note 9>&- on each):
|
|
82
|
+
# sh "$HOME/.agentschat/url-wake/start-receiver.sh" 9>&-
|
|
83
|
+
# sh "$HOME/.agentschat/url-wake/start-mcp-wake.sh" 9>&-
|
|
84
|
+
# Capture child output via a temp file, not $(...): a setsid daemon that
|
|
85
|
+
# inherits the capture pipe keeps $(...) waiting until the run cap kills us.
|
|
86
|
+
|
|
52
87
|
echo "example-url-wake-ensure.sh: documentation stub — wire start-receiver +" >&2
|
|
53
88
|
echo " start-mcp-wake for your host, then exit 0 when both are healthy." >&2
|
|
54
89
|
echo "See skill url-wake-keepalive and scripts/example-url-wake-receiver.mjs" >&2
|