agentschat-mcp 0.36.5 → 0.36.8
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 +55 -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 +28 -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/src/server.ts
CHANGED
|
@@ -51,11 +51,14 @@ import {
|
|
|
51
51
|
DEFAULT_GROK_BINDS_FILENAME,
|
|
52
52
|
boundProfileForConversation,
|
|
53
53
|
gateSwitchProfile,
|
|
54
|
+
grokBindApplies,
|
|
55
|
+
nonGrokWakeReason,
|
|
56
|
+
GROK_BINDS_META_FILENAME,
|
|
54
57
|
profileNameFromPath,
|
|
55
58
|
shouldHealBoundIdentity,
|
|
56
59
|
} from "./grok-bind.ts";
|
|
57
60
|
import { decideTermsConsent, TERMS_URL } from "./terms.ts";
|
|
58
|
-
import { annotateTools } from "./tool-annotations.ts";
|
|
61
|
+
import { annotateTools, TOOL_ANNOTATIONS } from "./tool-annotations.ts";
|
|
59
62
|
import { fireWake, fireGrokWake, resolveGrokAgentId, resolveGrokGatewayPath, grokBearerFromGatewayConfig, grokPortFromGatewayConfig } from "./wake.ts";
|
|
60
63
|
import pkg from "../package.json";
|
|
61
64
|
import {
|
|
@@ -69,6 +72,15 @@ import { join, dirname } from "path";
|
|
|
69
72
|
// Atomic + 0600-by-construction profile writer. Lives in its own module so the
|
|
70
73
|
// "the key is never world-readable, even mid-write" property is directly testable.
|
|
71
74
|
import { safeWriteProfile } from "./profile-store.ts";
|
|
75
|
+
import {
|
|
76
|
+
decideIdentityGuard,
|
|
77
|
+
advanceGuardRecord,
|
|
78
|
+
parseGuardRecord,
|
|
79
|
+
emptyGuardRecord,
|
|
80
|
+
IDENTITY_GUARD_FILENAME,
|
|
81
|
+
type GuardIdentity,
|
|
82
|
+
type IdentityGuardRecord,
|
|
83
|
+
} from "./identity-guard.ts";
|
|
72
84
|
// Read-cursor persistence: a state change that merely *lives* in the teardown path.
|
|
73
85
|
import { flushCursor, loadCursor, persistCursor } from "./read-cursor.ts";
|
|
74
86
|
import { randomUUID } from "crypto";
|
|
@@ -194,7 +206,7 @@ function listProfileFiles(): Array<{ name: string; path: string }> {
|
|
|
194
206
|
let files: string[] = [];
|
|
195
207
|
try { files = readdirSync(dir).filter((f: string) => f.endsWith(".json")); } catch {}
|
|
196
208
|
for (const file of files) {
|
|
197
|
-
if (file === DEFAULT_GROK_BINDS_FILENAME) continue; // bind map, not a profile
|
|
209
|
+
if (file === DEFAULT_GROK_BINDS_FILENAME || file === GROK_BINDS_META_FILENAME) continue; // bind map, not a profile
|
|
198
210
|
const name = file.replace(/\.json$/, "");
|
|
199
211
|
if (seen.has(name)) continue;
|
|
200
212
|
seen.add(name);
|
|
@@ -223,7 +235,13 @@ function resolveProfile(): { path: string; source: ProfileSource; declaredName?:
|
|
|
223
235
|
// is a declared identity (source grok-bind) so decideIdentity hard-errors
|
|
224
236
|
// rather than falling through to a sibling bot.
|
|
225
237
|
const grokToken = !!(cliArgs.token || process.env.AGENTCHAT_TOKEN);
|
|
226
|
-
const
|
|
238
|
+
const nonGrokWake = nonGrokWakeReason(process.env);
|
|
239
|
+
// A non-Grok wake stack (Antigravity / ZCode / URL wake) may have inherited a
|
|
240
|
+
// Grok agent's CURSOR_CONVERSATION_ID — never adopt that Grok identity.
|
|
241
|
+
const conversationId = nonGrokWake ? undefined : process.env.CURSOR_CONVERSATION_ID;
|
|
242
|
+
if (nonGrokWake && process.env.CURSOR_CONVERSATION_ID) {
|
|
243
|
+
process.stderr.write(`[agentchat] grok-bind skipped: non-grok wake (${nonGrokWake})\n`);
|
|
244
|
+
}
|
|
227
245
|
let binds: Record<string, string> = {};
|
|
228
246
|
if (!grokToken && conversationId) {
|
|
229
247
|
const bindPath = resolveGrokBindsPath(configDir, process.env.AGENTCHAT_GROK_BINDS);
|
|
@@ -541,6 +559,53 @@ if (!anonymousMode || explicitAgentId !== undefined || hasToken) {
|
|
|
541
559
|
}
|
|
542
560
|
}
|
|
543
561
|
|
|
562
|
+
// ── Identity guard (task_muk0154j_pstaosfn, incident 2026-09-27) ─────────────
|
|
563
|
+
// A host respawn after a WS bounce can resolve a DIFFERENT identity than this
|
|
564
|
+
// machine's MCP was trusted as (explicit selector lost → silent fall to the
|
|
565
|
+
// default profile — wrong-identity posts followed). Compare this boot's identity
|
|
566
|
+
// against the persisted trust record; on the default-fallback pattern, GATE
|
|
567
|
+
// write tools until whoami confirms. Precedence and registration are untouched.
|
|
568
|
+
let identityGateMessage = "";
|
|
569
|
+
const identityGuardFile = join(configDir, IDENTITY_GUARD_FILENAME);
|
|
570
|
+
function persistGuardRecord(rec: IdentityGuardRecord): void {
|
|
571
|
+
try {
|
|
572
|
+
mkdirSync(configDir, { recursive: true });
|
|
573
|
+
safeWriteProfile(identityGuardFile, rec);
|
|
574
|
+
} catch {}
|
|
575
|
+
}
|
|
576
|
+
function confirmLiveIdentityTrusted(): void {
|
|
577
|
+
identityGateMessage = "";
|
|
578
|
+
const live: GuardIdentity = {
|
|
579
|
+
agentId: AGENT_ID,
|
|
580
|
+
source: identity.mode === "env-creds" ? "env-creds" : profileSource,
|
|
581
|
+
...(declaredName ? { declaredName } : {}),
|
|
582
|
+
};
|
|
583
|
+
guardRecord = advanceGuardRecord(guardRecord, live, new Date().toISOString(), true);
|
|
584
|
+
persistGuardRecord(guardRecord);
|
|
585
|
+
}
|
|
586
|
+
let guardRecord: IdentityGuardRecord = (() => {
|
|
587
|
+
try { return parseGuardRecord(readFileSync(identityGuardFile, "utf-8")); } catch { return emptyGuardRecord(); }
|
|
588
|
+
})();
|
|
589
|
+
const guardBootIdentity: GuardIdentity = {
|
|
590
|
+
agentId: AGENT_ID,
|
|
591
|
+
source: identity.mode === "env-creds" ? "env-creds" : profileSource,
|
|
592
|
+
...(declaredName ? { declaredName } : {}),
|
|
593
|
+
};
|
|
594
|
+
{
|
|
595
|
+
const verdict = AGENT_ID
|
|
596
|
+
? decideIdentityGuard(guardRecord, guardBootIdentity)
|
|
597
|
+
: ({ kind: "ok" } as const); // anonymous: no identity to protect, nothing to persist
|
|
598
|
+
if (verdict.kind === "gate") {
|
|
599
|
+
identityGateMessage = verdict.message;
|
|
600
|
+
process.stderr.write(`\n[agentchat] 🚨 ${verdict.message}\n\n`);
|
|
601
|
+
guardRecord = advanceGuardRecord(guardRecord, guardBootIdentity, new Date().toISOString(), false);
|
|
602
|
+
} else {
|
|
603
|
+
if (verdict.kind === "warn") process.stderr.write(`[agentchat] WARNING: ${verdict.message}\n`);
|
|
604
|
+
guardRecord = advanceGuardRecord(guardRecord, guardBootIdentity, new Date().toISOString(), true);
|
|
605
|
+
}
|
|
606
|
+
if (AGENT_ID) persistGuardRecord(guardRecord);
|
|
607
|
+
}
|
|
608
|
+
|
|
544
609
|
// Update display name if provided via CLI
|
|
545
610
|
if (cliArgs.name && profile.display_name !== cliArgs.name) {
|
|
546
611
|
profile.display_name = cliArgs.name;
|
|
@@ -2059,6 +2124,25 @@ function loadGrokBinds(): Record<string, string> {
|
|
|
2059
2124
|
}
|
|
2060
2125
|
}
|
|
2061
2126
|
|
|
2127
|
+
/** Explicit profile declared at startup (env / --profile / --name), else null. */
|
|
2128
|
+
const explicitStartupProfileName: string | null =
|
|
2129
|
+
profileSource === "env" || profileSource === "legacy-env" || profileSource === "flag-profile" || profileSource === "flag-name"
|
|
2130
|
+
? (declaredName ?? null)
|
|
2131
|
+
: null;
|
|
2132
|
+
|
|
2133
|
+
/**
|
|
2134
|
+
* Whether grok-bind (switch lock + heal) applies to this process right now.
|
|
2135
|
+
* Non-Grok wakes and explicit --profile ≠ bound profile are exempt.
|
|
2136
|
+
*/
|
|
2137
|
+
function currentGrokBindApplicability(binds: Record<string, string>) {
|
|
2138
|
+
return grokBindApplies({
|
|
2139
|
+
env: process.env,
|
|
2140
|
+
explicitProfileName: explicitStartupProfileName,
|
|
2141
|
+
conversationId: process.env.CURSOR_CONVERSATION_ID,
|
|
2142
|
+
binds,
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2062
2146
|
/**
|
|
2063
2147
|
* Apply a profile file as the live identity and reconnect WS.
|
|
2064
2148
|
* Shared by `switch_profile` and grok-bind heal.
|
|
@@ -2080,6 +2164,15 @@ function applyIdentityFromProfile(newProfile: any, targetFile: string): void {
|
|
|
2080
2164
|
profile = newProfile;
|
|
2081
2165
|
activeProfileFile = targetFile;
|
|
2082
2166
|
anonymousMode = false;
|
|
2167
|
+
// An explicit in-process switch is a TRUSTED identity change — advance the
|
|
2168
|
+
// identity-guard record so the next boot doesn't alarm on it.
|
|
2169
|
+
guardRecord = advanceGuardRecord(
|
|
2170
|
+
guardRecord,
|
|
2171
|
+
{ agentId: AGENT_ID, source: "switch_profile", ...(profileNameFromPath(targetFile) ? { declaredName: profileNameFromPath(targetFile)! } : {}) },
|
|
2172
|
+
new Date().toISOString(),
|
|
2173
|
+
true,
|
|
2174
|
+
);
|
|
2175
|
+
persistGuardRecord(guardRecord);
|
|
2083
2176
|
wsReconnectAttempt = 0;
|
|
2084
2177
|
heartbeat.start();
|
|
2085
2178
|
connectWS();
|
|
@@ -2093,7 +2186,9 @@ function applyIdentityFromProfile(newProfile: any, targetFile: string): void {
|
|
|
2093
2186
|
function ensureGrokBoundIdentity(): void {
|
|
2094
2187
|
// Runtime recovery must honor the same explicit identity precedence as startup.
|
|
2095
2188
|
if (profileSource !== "grok-bind" || hasToken || cliArgs.id || process.env.AGENTCHAT_AGENT_ID) return;
|
|
2096
|
-
const
|
|
2189
|
+
const healBinds = loadGrokBinds();
|
|
2190
|
+
if (!currentGrokBindApplicability(healBinds).applies) return;
|
|
2191
|
+
const boundName = boundProfileForConversation(process.env.CURSOR_CONVERSATION_ID, healBinds);
|
|
2097
2192
|
if (!boundName) return;
|
|
2098
2193
|
const boundPath = nameToPath(boundName);
|
|
2099
2194
|
if (!existsSync(boundPath)) return;
|
|
@@ -2139,6 +2234,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2139
2234
|
// stole the live profile via switch_profile. No-op when unbound.
|
|
2140
2235
|
ensureGrokBoundIdentity();
|
|
2141
2236
|
|
|
2237
|
+
// Identity guard (task_muk0154j_pstaosfn): after a suspect fallback (explicit
|
|
2238
|
+
// identity lost on respawn → default profile), WRITE tools refuse until the
|
|
2239
|
+
// agent looks at whoami and confirms. Read-only tools and whoami stay open.
|
|
2240
|
+
if (identityGateMessage && name !== "whoami" && TOOL_ANNOTATIONS[name]?.readOnlyHint !== true) {
|
|
2241
|
+
return { content: [{ type: "text", text: `🚨 ${identityGateMessage}` }], isError: true };
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2142
2244
|
if (name === "list_skills") {
|
|
2143
2245
|
const { chat_id } = (args || {}) as { chat_id?: string };
|
|
2144
2246
|
const out: any = {
|
|
@@ -3052,7 +3154,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3052
3154
|
authLine = r.ok ? "REST auth: ok" : `REST auth: failed (${r.status})`;
|
|
3053
3155
|
} catch { authLine = "REST auth: unknown"; }
|
|
3054
3156
|
}
|
|
3055
|
-
|
|
3157
|
+
// Identity guard: a whoami call IS the confirmation. Surface the gate state
|
|
3158
|
+
// prominently on this response, then trust the live identity and unblock writes.
|
|
3159
|
+
const gateNotice = identityGateMessage
|
|
3160
|
+
? `🚨 IDENTITY GUARD: ${identityGateMessage}\n(This whoami call confirms identity — writes are now unblocked. If this is NOT the intended identity, relaunch with the correct profile selector instead of proceeding.)\n\n`
|
|
3161
|
+
: "";
|
|
3162
|
+
const wasGated = identityGateMessage !== "";
|
|
3163
|
+
const text = `Profile: ${profile.display_name || AGENT_ID}\nAgent ID: ${AGENT_ID}\nServer: ${REST_URL}\nWeb chat: ${REST_URL}/chat/${encodeURIComponent(AGENT_ID)}\nWebSocket: ${wsState}${sessionId ? `\nSession: ${sessionId.slice(0, 12)}...` : ""}\n${healthLine}\n${authLine}\n${claimedLine}${claimHint ? `\n${claimHint}` : ""}\nCapabilities: ${CAPABILITIES.join(", ")}\nProfile file: ${activeProfileFile ?? (anonymousMode ? "(none — anonymous, no profile written)" : "(none — credentials from environment)")}`;
|
|
3164
|
+
if (wasGated) confirmLiveIdentityTrusted();
|
|
3165
|
+
return { content: [{ type: "text", text: gateNotice + text }] };
|
|
3056
3166
|
}
|
|
3057
3167
|
|
|
3058
3168
|
if (name === "list_channels") {
|
|
@@ -3227,9 +3337,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3227
3337
|
// when set, or via current profile when Cursor stdio started with --profile
|
|
3228
3338
|
// and no CURSOR_CONVERSATION_ID. Refuse switches that leave the locked
|
|
3229
3339
|
// identity (including other Grok bots or Hermes/Spiral). No-op allowed.
|
|
3230
|
-
|
|
3340
|
+
// Non-Grok wakes (Antigravity / ZCode) and explicit --profile ≠ bound
|
|
3341
|
+
// profile are exempt: an inherited CURSOR_CONVERSATION_ID must not lock them.
|
|
3342
|
+
const switchBinds = loadGrokBinds();
|
|
3343
|
+
const bindApplies = currentGrokBindApplicability(switchBinds);
|
|
3344
|
+
const switchGate = !bindApplies.applies ? ({ kind: "allow" } as const) : gateSwitchProfile({
|
|
3231
3345
|
conversationId: process.env.CURSOR_CONVERSATION_ID,
|
|
3232
|
-
binds:
|
|
3346
|
+
binds: switchBinds,
|
|
3233
3347
|
requestedProfileName: profile_name,
|
|
3234
3348
|
currentProfileName: profileNameFromPath(activeProfileFile),
|
|
3235
3349
|
});
|