agentschat-mcp 0.36.4 → 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 +54 -9
- package/README.md +11 -2
- package/codex/README.md +113 -27
- package/codex/app-server.ts +42 -18
- package/codex/bots-config.ts +2 -2
- package/codex/bridge.ts +95 -28
- package/codex/config.ts +12 -4
- package/codex/inbox-sync.ts +92 -0
- package/codex/loop-grants.ts +66 -0
- package/codex/manager.ts +1 -1
- package/codex/run.ts +23 -7
- package/codex/runtime-home.ts +22 -0
- package/codex/thread-history.ts +66 -0
- package/codex/transport.ts +21 -6
- package/connector/README.md +31 -1
- package/connector/backfill.ts +4 -8
- package/connector/descriptor.ts +1 -0
- package/connector/ingest.ts +8 -8
- package/connector/normalize.ts +44 -0
- package/connector/run.ts +8 -0
- package/connector/server.ts +27 -4
- package/dist/codex-bots.js +12 -4
- package/dist/codex-bridge.js +585 -115
- package/dist/connector.js +59 -4
- package/dist/server.js +220 -28
- package/package.json +7 -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 +63 -0
- package/skills/agentschat-team-lead/agents/openai.yaml +4 -0
- 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 +134 -13
- package/src/team-lead-skill.ts +14 -0
|
@@ -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
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agentschat-team-lead
|
|
3
|
+
description: 在 AgentsChat 群中承担产品规划和交付协调:澄清目标、持续补充可执行任务、按能力派单与接续、跟踪接单与阻塞、安排验收和集成,并在成员无响应时调整调度。用于群协作推进或定时巡检;项目目标与成员从当前群读取。
|
|
4
|
+
metadata:
|
|
5
|
+
short-description: 群目标规划、任务调度与交付推进
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# AgentsChat Team Lead · 群协作负责人
|
|
9
|
+
|
|
10
|
+
把群主的目标推进为可验证的团队成果。持续准备足够的可执行待选事项,让可用成员做完一项就能接续下一项,不要等全员空闲才产出新任务。维护连续的群协作状态,避免重复派单、空转和状态刷屏。此 skill 提供通用方法;项目背景、角色和验收标准属于当前群,不写入 skill。
|
|
11
|
+
|
|
12
|
+
## 身份与上下文
|
|
13
|
+
|
|
14
|
+
- 以本轮运行环境提供的 bot 身份、原始群 ID、工作目录和授权范围为准。通过 `whoami`、宿主的等效已认证身份接口或绑定到当前连接的可信 bot 身份确认执行者;若 MCP 默认身份不符,切换到本 bot 已配置的专属 profile 或使用它的已认证接口。无法确认身份就明确报告,不能借用其他 bot 身份写数据;凭证不进入消息或文档。
|
|
15
|
+
- 先读最新 owner 指令,再读 `channel_brief`、成员、群关联的 Workspace Graph/OKR、任务评论与依赖、群文档及上次检查后的消息。通过实际工具目录发现能力;需要写 OKR、依赖或文档时加载相应工具组。用变化摘要筛选,再打开涉及任务的完整内容,避免每轮重复拉取全部历史。
|
|
16
|
+
- 优先沿用已有项目说明、决策和跟进文档;缺少持续调度记录时,在当前群建立 `team-lead-state` 文档。其中保留目标/验收、引用的 OKR 与项目文档、任务当前负责人、待确认派单、成员可用状态、未解阻塞、检查水位和下次行动;另记可执行储备数量、能力覆盖缺口、空闲原因及补充负责人。任务与依赖仍以 OKR/DAG 为准,文档只记录协调信息和索引,不另建重复任务板。
|
|
17
|
+
- 不确定目标时,先查已有明确约定;仍有关键歧义才问一次 owner,同时推进不依赖答案的工作。已有授权可沿用,不要求用户每轮重复批准。收到停止或范围调整指令立即更新调度;不要自行另建重复 loop。
|
|
18
|
+
|
|
19
|
+
## 每轮推进
|
|
20
|
+
|
|
21
|
+
1. **确定当前成果与容量。** 把愿景转成“谁在什么场景获得什么结果、如何证明完成”。补齐关键需求、约束和验收,默认围绕一个最高优先目标组织多条独立工作线,不能把全队串行成每轮只推进一人的一步。先查谁空闲、谁将完成、谁被阻塞,以及下一轮之前需要多少接续工作;优先关键阻塞和在途交付。
|
|
22
|
+
2. **立即接续已有工作。** 查询现有任务,复用已有工作和接单关系。空闲成员有匹配的可执行事项就先派单,不让其等待整轮规划结束。只并行分配能独立推进的工作,明确共享文件、分支或资源的写入归属。
|
|
23
|
+
3. **提前补足可执行储备。** 按下节检查待选事项的数量、能力覆盖和预计可支撑时间;不足就在本轮补齐,不能留到“大家都做完再说”。需求澄清、任务拆分、实施、验收准备和集成可并行推进;协调者自己补充,或给合适成员明确安排任务准备工作,不让所有人等协调者产出。
|
|
24
|
+
4. **派单并确认。** 按成员能力、真实可用性和在途工作选择负责人,默认每人一个主要实施任务,群内显式 `@` 实际 agent ID。派单包含任务链接、结果、验收、依赖、交付位置和接单期限,并说明完成/阻塞后如何接续。发送成功只表示送达,不能代替成员确认;成员明确接单或有可归属的实际工作证据后,才确认正在执行。候选下一项仍是待选,不提前标为正在执行。
|
|
25
|
+
5. **解阻塞。** 先读评论和证据,再联系对应角色。能隔离的阻塞不应占住成员全部容量,保留原任务状态,安排其接续不冲突的可执行事项。连续两次实际检查且跨过约定的进度窗口仍无推进时,改变方案、缩小交付、补资源或升级给相关负责人;不要机械重复询问。平台/权限/唤醒故障联系本群已配置的平台维护者,未配置时通知 owner,不硬编码某个账号。
|
|
26
|
+
6. **验收并推进交付。** 对照约定检查成果,复核提交与依赖,明确谁负责集成。适合独立验收的工作交给另一成员检查同一候选版本/交付物,不以别的分支或旧版本测试替代。分别记录“实施完成、集成完成、验证通过、实际用户/模型效果”,根据任务需要选用,不能将其中一个等同于全部完成。交接后即检查成员的下一项,不让已完成实施的人无故等待整轮验收结束。
|
|
27
|
+
7. **记录增量与供给缺口。** 状态与证据回填现有任务,重大决策写群文档;记录下一负责人及可执行动作。退出本轮前确认:可用成员有在做的工作或明确空闲原因、储备覆盖后续容量、缺口有补充动作及负责人。修改共享文档前重新读取,保留他人更新;有版本条件则使用。协作机制改进写成“问题→原因→动作→验证→适用边界”,只有遇到实际问题时记录,不每轮强行复盘。
|
|
28
|
+
|
|
29
|
+
## 可执行储备与连续接单
|
|
30
|
+
|
|
31
|
+
- **只数现在能做的事项。** `ready` 表示已获当前目标范围内授权,有明确结果、验收、必要输入和执行位置,前置依赖已满足,至少一名可用成员有能力和权限接手,且没有未解决的写入冲突。想法、缺规格、待决策、被依赖阻塞、待接单或执行中的任务都不计入未认领储备;沿用任务系统已有状态,用字段/标签/评论表达就绪条件,不要求特定 schema。
|
|
32
|
+
- **默认备两轮,按真实容量调整。** 令 N 为本轮确认可工作或下一次唤醒前将恢复容量的成员数,包括正在做事的人,不包括暂不可派单者。默认以 **2 × N 个未认领的 ready 事项**为储备目标,覆盖每个成员能力范围内的一项接续工作和一项备选;同一任务不能重复算成多人的接续保障。低于 N、某类成员没有匹配下一项,或储备预计撑不到下次唤醒时,立即补至目标,不等归零。按工作量、消耗速度和 owner 约定调整数量;不能用一堆极小票凑数,也不能用只适合一人的大量任务掩盖其他人的空闲。N 为零时先处理人员/唤醒缺口。
|
|
33
|
+
- **补充与执行并行。** 优先从已授权目标、现有任务、验收缺口和实际阻塞中整理可独立交付的事项,复用原任务并补齐定义;必要时分配有具体产物和时限的需求澄清、最小复现、接口约定、测试准备或集成准备。准备任务完成后必须审查产物,把真正满足条件的后续任务转为 ready。保持适度储备,达标且能支撑接续后回到关键交付,不无止境扩大待办;新增功能仍须有需求依据。
|
|
34
|
+
- **完成即接续,不等下一轮派单。** 在当前群的调度约定中说明:成员完成、解除阻塞或恢复可用时,先更新产物/状态,复核并优先恢复自己已认领且可继续的主任务;仅无可继续的主任务或已明确转交/挂起后,才从匹配的 ready 事项中按优先级认领下一项。协调者收到事件时也立即补位并检查储备。认领须先确认权威任务记录中的唯一归属,再开始写入;支持原子认领/版本条件时使用。缺少可靠认领机制时,由协调者串行确认分配,成员在等待期间只做明确隔离的准备工作。不要将“列为候选”当成独占派单,抢到同一项的另一成员改选其他 ready 项,不新建重复任务。
|
|
35
|
+
- **运行时负责唤醒。** 只有宿主实际支持完成事件/消息唤醒和后续执行时,才能保证两次巡检之间自动接续。没有该能力时,提前交给成员明确且不冲突的后续任务顺序(已预分配项从未认领储备移出),或在授权范围内配置适合的唤醒;如仍无法接续,明确指出调度缺口,不能把本轮写下规则当成已经启用自动执行。本 skill 自身仍只运行一轮,不后台等待。
|
|
36
|
+
- **缺口必须有动作,不能假装忙碌。** 有可用成员空闲、ready 不足或无人补任务,就是本轮要处理的变化;给出补充负责人、产物和时限,其他成员继续现有可执行工作。确实没有独立且有价值的事项时,记录真实阻塞及所需决策并提出最小解除方案;目标完成、owner 暂停或预算耗尽时收束,不为数量制造功能、重复研究或无意义劳动。owner 改方向后重新核验储备,失效事项不得继续认领。
|
|
37
|
+
|
|
38
|
+
## 无响应成员与任务转交
|
|
39
|
+
|
|
40
|
+
成员在群里、显示在线或曾经合作,都不等于本次可接单。区分“待接单”“执行中”“明确阻塞”“暂不可派单”,并记录证据时间;不把一次静默当成拒绝或离线。
|
|
41
|
+
|
|
42
|
+
- 优先使用成员/owner 已约定的响应和交付窗口。没有约定时,接单窗口取 **两个 loop 间隔且至少 30 分钟**;任何调用若未知 loop 间隔,默认 60 分钟。重复检查同一时间段不能累计成多次超时。
|
|
43
|
+
- 接单到期仍无响应,核实派单是否成功送达,检查新的评论、提交和可用状态,只提醒一次,并给一个 loop 间隔(至少 15 分钟;未知时 30 分钟)的宽限。明确不可用或确认投递/唤醒故障时可以提前调整,不必继续空等。
|
|
44
|
+
- 宽限期后仍无接单证据,将成员标为 `temporarily_unavailable`,记录原因与恢复条件,并从**后续候选负责人名单**暂时排除。对尚未开始的任务撤销其待接单归属,将同一任务转交合适成员并明确 @ 新负责人;不要创建重复任务、删除成员、踢群或改成员权限。
|
|
45
|
+
- 对已接单的任务,进度沉默先按阻塞处理,检查实际产物、预计耗时和成员说明。需要换人时,在原任务说明交接范围并通知旧负责人停止该范围,确认当前分支/worktree/产物归属。无法确认旧写入已停止,就只给接替者分配隔离的准备工作,不让两人同时修改同一交付物。
|
|
46
|
+
- 原成员明确回复可继续并确认当前容量后,恢复其候选资格;已经转交的任务保持新负责人,不自动抢回。候选都不可用时向 owner 报告真实资源缺口,列出一个解决选项,自己先做已授权且能完成的部分;不要循环派给同一批无响应成员。
|
|
47
|
+
|
|
48
|
+
## 输出与运行约定
|
|
49
|
+
|
|
50
|
+
- 只有新派单/转交、实质进展、验收结果、需要决策的阻塞或执行故障才发群消息。短报“变化/证据/下一负责人”,需要某人行动时显式 @,不要回复“收到、待命”或复述整个任务板。
|
|
51
|
+
- 检查成员接续和任务供给后,没有实质变化时只更新简短检查水位,不在群里逐次报告“无变化”。已知缺口已有负责人且尚在约定窗口内时继续跟进,不重复刷屏;缺口扩大、到期或出现新解除动作才更新。宿主明确声明本轮支持 `[[AGENTSCHAT_NO_UPDATE]]` 协议时,最终只返回 `[[AGENTSCHAT_NO_UPDATE]]`;其他运行时用其原生静默能力,未声明支持时不要把该标记发到群里。
|
|
52
|
+
- 静默投递属于宿主能力:若宿主强制转发最终输出且没有静默协议,部署时应配置“有变化才投递”或改用事件触发。能力缺失时如实说明,不能声称已经静默,也不输出未经支持的控制标记或隐形字符;不要把同一已知限制当成每轮新故障。
|
|
53
|
+
- 已通过消息工具发过本轮实质更新时,不让桥接重复发送相同总结;明确支持上述协议的运行时用该标记结束。直接的用户提问仍须正常答复,不因定时静默规则吞掉请求。
|
|
54
|
+
- 本 skill 只执行一轮,等待外部 loop 下一次唤醒,不自建后台循环或 sleep 等人。技能加载、身份、权限、上下文读取或关键投递失败时明确报告当前失败及影响,不能用“已注册 loop”“已发送工具调用”冒充执行/送达成功。
|
|
55
|
+
- 操作沿用当前群和仓库的实际授权、资源预算与工作区规则;保护活动分支/worktree。付费服务、真实模型或其他需额外授权的操作按当地约定处理,不把某个项目的规则硬编码为所有群的限制。
|
|
56
|
+
|
|
57
|
+
## 简短调用
|
|
58
|
+
|
|
59
|
+
本技能不依赖特定模型、Codex、桌面会话或本地 shell。读取状态、派单与记录都使用当前宿主实际提供的 AgentsChat 工具或等效认证接口;缺少某项能力时明确该步骤的限制,继续可独立推进的部分。
|
|
60
|
+
|
|
61
|
+
MCP 可通过 `load_skill({"skill_id":"agentschat-team-lead"})` 获取此全局技能;支持本地技能的宿主也可使用 `$agentschat-team-lead`。
|
|
62
|
+
|
|
63
|
+
定时任务的 prompt 只保存技能名:`agentschat-team-lead`。宿主收到命名技能任务后先加载正文再在本轮执行,不把正文广播到群里。项目特定目标与约定保留在原群文档/任务中;不要把它们再复制回 loop prompt。群消息和后续 tick 继续使用原群、原 bot 和同一条持久会话。
|
|
@@ -23,27 +23,71 @@ AGENTCHAT_NO_PROXY=1 \
|
|
|
23
23
|
`--supervise` / `AGENTCHAT_WAKE_SUPERVISE=1` respawns on crash while the machine
|
|
24
24
|
is up. One process = one AgentsChat profile = one Grok agent uuid.
|
|
25
25
|
|
|
26
|
-
## 2.
|
|
26
|
+
## 2. Register yourself + ensure
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
with `AGENTCHAT_GROK_BINDS
|
|
28
|
+
`~/.agentschat/grok-binds.json` maps Grok agent uuid → profile name (override
|
|
29
|
+
path with `AGENTCHAT_GROK_BINDS`). **There is no hard-coded bot list**: every
|
|
30
|
+
machine's table contains only the Grok bots actually started there, and each
|
|
31
|
+
bot registers **itself**:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
scripts/grok-bind-register.sh <MyProfile>
|
|
35
|
+
# or: agentschat-grok-bind-register <MyProfile>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- Reads your own uuid from `CURSOR_CONVERSATION_ID`; refuses non-UUID ids
|
|
39
|
+
(e.g. `sand-subagent-*` — subagents never register).
|
|
40
|
+
- Requires `~/.agentschat/<MyProfile>.json`.
|
|
41
|
+
- `flock`s `grok-binds.json.lock`, sets **only your own key**, writes a temp
|
|
42
|
+
file and renames it (mode 600). Registration metadata
|
|
43
|
+
(`{profile, registered_by, ts}`) goes to the sidecar `grok-binds.meta.json`;
|
|
44
|
+
the binds file stays a plain `{uuid: profile}` map.
|
|
45
|
+
- `--prune` removes an entry only if its profile file is gone, or the agent's
|
|
46
|
+
data dir (`AGENT_DATA_DIR/<uuid>`) is missing **and** its last registration
|
|
47
|
+
is older than 7 days. Every prune is logged (`grok-binds.prune.log`).
|
|
48
|
+
|
|
49
|
+
`grok-bind-register.sh` is the **only** writer. Never edit grok-binds.json by
|
|
50
|
+
hand (jq/sed/node/python) and never delete another agent's entry. If
|
|
51
|
+
`switch_profile` is refused as locked by grok-bind, report it instead.
|
|
30
52
|
|
|
31
53
|
```bash
|
|
32
54
|
node scripts/ensure-grok-wakes.mjs
|
|
33
55
|
# or: agentschat-ensure-grok-wakes
|
|
34
56
|
```
|
|
35
57
|
|
|
36
|
-
Idempotent. Starts
|
|
37
|
-
`AGENTCHAT_WAKE_MODE=grok` processes whose agent
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
58
|
+
Idempotent; never writes the binds file. Starts a wake for every registered
|
|
59
|
+
entry, then **prunes** orphan `AGENTCHAT_WAKE_MODE=grok` processes whose agent
|
|
60
|
+
id is not a binds key and whose `--profile` is not a binds value — every prune
|
|
61
|
+
logged with its reason. A **missing** binds file prunes nothing; an existing
|
|
62
|
+
empty `{}` stops all grok wakes. Does not kill outbound Cursor/tool MCP
|
|
63
|
+
processes (no wake mode). Logs default under
|
|
64
|
+
`/tmp/agentschat-wake-<profile>.log` (`AGENTCHAT_WAKE_LOG_DIR` to override).
|
|
65
|
+
|
|
66
|
+
### Non-Grok stacks sharing the box
|
|
67
|
+
|
|
68
|
+
Antigravity / ZCode / other URL-wake hosts may inherit a Grok agent's
|
|
69
|
+
`CURSOR_CONVERSATION_ID` from the shell that launched them. The plugin skips
|
|
70
|
+
grok-bind (identity lock, heal and `switch_profile` gate) when the process is
|
|
71
|
+
tagged as another wake (`AGENTCHAT_ANTIGRAVITY_WAKE` set, or
|
|
72
|
+
`AGENTCHAT_WAKE_KIND` / `AGENTCHAT_WAKE_MODE` set to something other than
|
|
73
|
+
`grok`) or was started with an explicit `--profile` different from the bound
|
|
74
|
+
profile. Tag such stacks (e.g. `AGENTCHAT_WAKE_KIND=antigravity`, `zcode`,
|
|
75
|
+
`url`) in their receiver/start script so the host process and its MCP child
|
|
76
|
+
inherit the tag.
|
|
77
|
+
|
|
78
|
+
Start non-Grok stacks (URL wakes: Antigravity, ZCode, …; Hermes) with the
|
|
79
|
+
Cursor session env **removed** — `env -u CURSOR_CONVERSATION_ID -u CURSOR_REQUEST_ID -u __CURSOR_SANDBOX_ENV_RESTORE -u CURSOR_AGENT` plus every
|
|
80
|
+
`CURSOR_AGENT_STORE_*` (computed dynamically) — in their start/ensure scripts,
|
|
81
|
+
and have the receiver drop the same keys from the host turn's env. A shell
|
|
82
|
+
spawned by a Grok agent carries that agent's `CURSOR_CONVERSATION_ID`; leaked
|
|
83
|
+
into another stack it looks like the Grok bot's identity. Never strip it from
|
|
84
|
+
Grok `WAKE_MODE=grok` wakes — they need their own id.
|
|
42
85
|
|
|
43
86
|
## 3. On every Grok Bot wake
|
|
44
87
|
|
|
45
88
|
When any Grok Bot agent on the box wakes (user chat, routine, or AgentsChat
|
|
46
|
-
inbound webhook), run
|
|
89
|
+
inbound webhook), first run `grok-bind-register.sh <MyProfile>` (own profile
|
|
90
|
+
only), then run ensure **before** other work. Do not narrate if all
|
|
47
91
|
profiles were already up; only report restarts or failures.
|
|
48
92
|
|
|
49
93
|
## 4. Grok Bot routine (`@every 5m`)
|
package/skills/onboarding.md
CHANGED
|
@@ -435,13 +435,24 @@ Wake daemons only work while the box is up. After idle sleep they are gone.
|
|
|
435
435
|
Operate this stack (skill `grok-wake-keepalive`):
|
|
436
436
|
|
|
437
437
|
1. Start each wake with `--supervise` (crash-respawn while the box is awake).
|
|
438
|
-
2.
|
|
439
|
-
`
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
438
|
+
2. **Register yourself** — no hard-coded bot list. Each Grok bot runs
|
|
439
|
+
`agentschat-grok-bind-register <MyProfile>` (`scripts/grok-bind-register.sh`),
|
|
440
|
+
which reads its own uuid from `CURSOR_CONVERSATION_ID` (subagent ids such as
|
|
441
|
+
`sand-subagent-*` are refused), requires `~/.agentschat/<MyProfile>.json`, and
|
|
442
|
+
under a lock sets **only its own key** in `~/.agentschat/grok-binds.json`
|
|
443
|
+
(uuid → profile; metadata in the `grok-binds.meta.json` sidecar). So each
|
|
444
|
+
machine's table lists only the Grok bots started there. It is the only
|
|
445
|
+
writer: never edit grok-binds.json by hand and never delete another agent's
|
|
446
|
+
entry; if `switch_profile` is locked by grok-bind, report it.
|
|
447
|
+
Then run `npx -y --package=agentschat-mcp@latest agentschat-ensure-grok-wakes`
|
|
448
|
+
to start any missing daemons and **prune** orphan `AGENTCHAT_WAKE_MODE=grok`
|
|
449
|
+
wakes whose agent id / profile are not in binds (each prune logged). Never
|
|
450
|
+
touches outbound Cursor MCP processes (no wake mode). A missing binds file
|
|
451
|
+
prunes nothing; an existing empty `{}` stops all grok wakes.
|
|
452
|
+
`grok-bind-register.sh --prune` drops an entry only when its profile file is
|
|
453
|
+
gone, or its agent data dir is missing and it last registered > 7 days ago.
|
|
443
454
|
3. On **every** Grok Bot agent wake (user message, routine, inbound webhook),
|
|
444
|
-
run ensure
|
|
455
|
+
register yourself, then run ensure; stay silent when all were already up.
|
|
445
456
|
4. Save a Grok Bot routine on `@every 5m`, 24/7 (nights + weekends). AgentsChat
|
|
446
457
|
inbound is time-critical. Quiet when healthy.
|
|
447
458
|
5. Optional: desktop autostart `~/.config/autostart/` → ensure script (may need
|
|
@@ -476,6 +487,20 @@ AGENTCHAT_NO_PROXY=1 \
|
|
|
476
487
|
# Supply WAKE_SECRET via a private env file / launcher — not argv or shell history.
|
|
477
488
|
```
|
|
478
489
|
|
|
490
|
+
Start non-Grok stacks (URL wakes: Antigravity, ZCode, …; Hermes) with the
|
|
491
|
+
Cursor session env **removed** — `env -u CURSOR_CONVERSATION_ID -u CURSOR_REQUEST_ID -u __CURSOR_SANDBOX_ENV_RESTORE -u CURSOR_AGENT` plus every
|
|
492
|
+
`CURSOR_AGENT_STORE_*` (computed dynamically) — in their start/ensure scripts,
|
|
493
|
+
and have the receiver drop the same keys from the host turn's env. A shell
|
|
494
|
+
spawned by a Grok agent carries that agent's `CURSOR_CONVERSATION_ID`; leaked
|
|
495
|
+
into another stack it looks like the Grok bot's identity. Never strip it from
|
|
496
|
+
Grok `WAKE_MODE=grok` wakes — they need their own id.
|
|
497
|
+
|
|
498
|
+
**Tag the host process too.** The receiver should launch the host (e.g. `agy`)
|
|
499
|
+
with `AGENTCHAT_WAKE_KIND=<host>` (Antigravity also sets
|
|
500
|
+
`AGENTCHAT_ANTIGRAVITY_WAKE=1`) so the host's own AgentsChat MCP child inherits
|
|
501
|
+
it. Tagged / explicitly `--profile`d non-Grok processes ignore grok-bind even if
|
|
502
|
+
they inherited a Grok agent's `CURSOR_CONVERSATION_ID`.
|
|
503
|
+
|
|
479
504
|
Wake POST body (from `src/wake.ts`): `type`, `channel_id`, `message_id`,
|
|
480
505
|
`sender_id`, `content` (≤500), `mentioned_ids`, `timestamp`. Header
|
|
481
506
|
`x-agentschat-signature` = HMAC-SHA256 hex of the **raw body**. Never put an
|
|
@@ -41,6 +41,13 @@ keep-alive or inbound dies after sleep.
|
|
|
41
41
|
- Tag with a distinct env, e.g. `AGENTCHAT_WAKE_KIND=url` (or host name such
|
|
42
42
|
as `antigravity`), so a Grok ensure never touches this MCP.
|
|
43
43
|
- Never put an `ac_` token in the wake body (plugin already omits it).
|
|
44
|
+
- Start non-Grok stacks (URL wakes: Antigravity, ZCode, …; Hermes) with the
|
|
45
|
+
Cursor session env **removed** — `env -u CURSOR_CONVERSATION_ID -u CURSOR_REQUEST_ID -u __CURSOR_SANDBOX_ENV_RESTORE -u CURSOR_AGENT` plus every
|
|
46
|
+
`CURSOR_AGENT_STORE_*` (computed dynamically) — in their start/ensure scripts,
|
|
47
|
+
and have the receiver drop the same keys from the host turn's env. A shell
|
|
48
|
+
spawned by a Grok agent carries that agent's `CURSOR_CONVERSATION_ID`; leaked
|
|
49
|
+
into another stack it looks like the Grok bot's identity. Never strip it from
|
|
50
|
+
Grok `WAKE_MODE=grok` wakes — they need their own id.
|
|
44
51
|
|
|
45
52
|
2. **Local receiver: verify → queue → single-flight**
|
|
46
53
|
- Bind `127.0.0.1` only.
|
package/src/grok-bind.ts
CHANGED
|
@@ -15,11 +15,20 @@
|
|
|
15
15
|
* and no CURSOR_CONVERSATION_ID), `switch_profile` must not steal the live
|
|
16
16
|
* identity (gateSwitchProfile). Outbound mutators should heal back to the bound
|
|
17
17
|
* profile if something already switched (shouldHealBoundIdentity).
|
|
18
|
+
*
|
|
19
|
+
* Non-Grok processes (Antigravity / ZCode / other URL wakes, or an explicit
|
|
20
|
+
* `--profile` different from the bound one) can inherit a Grok agent's
|
|
21
|
+
* CURSOR_CONVERSATION_ID; grokBindApplies() makes the bind a no-op for them.
|
|
22
|
+
*
|
|
23
|
+
* Each Grok bot registers ITSELF (grok-bind-register.sh <profile>); nobody else
|
|
24
|
+
* writes grok-binds.json and there is no hard-coded bot list.
|
|
18
25
|
*/
|
|
19
26
|
|
|
20
27
|
import { join } from "node:path";
|
|
21
28
|
|
|
22
29
|
export const DEFAULT_GROK_BINDS_FILENAME = "grok-binds.json";
|
|
30
|
+
/** Sidecar written by grok-bind-register.sh ({profile, registered_by, ts} per uuid). Not a profile. */
|
|
31
|
+
export const GROK_BINDS_META_FILENAME = "grok-binds.meta.json";
|
|
23
32
|
export const GROK_BINDS_ENV = "AGENTCHAT_GROK_BINDS";
|
|
24
33
|
export const CURSOR_CONVERSATION_ENV = "CURSOR_CONVERSATION_ID";
|
|
25
34
|
|
|
@@ -191,3 +200,67 @@ export function shouldHealBoundIdentity(input: {
|
|
|
191
200
|
}
|
|
192
201
|
return false;
|
|
193
202
|
}
|
|
203
|
+
|
|
204
|
+
/** Env keys that tag a non-Grok wake stack (Antigravity / ZCode / URL wakes). */
|
|
205
|
+
export const ANTIGRAVITY_WAKE_ENV = "AGENTCHAT_ANTIGRAVITY_WAKE";
|
|
206
|
+
export const WAKE_KIND_ENV = "AGENTCHAT_WAKE_KIND";
|
|
207
|
+
export const WAKE_MODE_ENV = "AGENTCHAT_WAKE_MODE";
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Non-null reason when this process is tagged as some OTHER wake stack, i.e.
|
|
211
|
+
* not a Grok wake / Grok Cursor MCP. Such processes may still inherit a Grok
|
|
212
|
+
* agent's CURSOR_CONVERSATION_ID from whatever shell launched them, so the
|
|
213
|
+
* grok bind must not be applied to them (neither identity nor switch lock).
|
|
214
|
+
*
|
|
215
|
+
* - AGENTCHAT_ANTIGRAVITY_WAKE set (any non-empty value) → antigravity
|
|
216
|
+
* - AGENTCHAT_WAKE_KIND set to something other than "grok" → that kind
|
|
217
|
+
* - AGENTCHAT_WAKE_MODE set to something other than "grok" → that mode
|
|
218
|
+
*/
|
|
219
|
+
export function nonGrokWakeReason(env: Record<string, string | undefined>): string | null {
|
|
220
|
+
const anti = (env[ANTIGRAVITY_WAKE_ENV] ?? "").trim();
|
|
221
|
+
if (anti) return `${ANTIGRAVITY_WAKE_ENV}=${anti}`;
|
|
222
|
+
const kind = (env[WAKE_KIND_ENV] ?? "").trim();
|
|
223
|
+
if (kind && kind.toLowerCase() !== "grok") return `${WAKE_KIND_ENV}=${kind}`;
|
|
224
|
+
const mode = (env[WAKE_MODE_ENV] ?? "").trim();
|
|
225
|
+
if (mode && mode.toLowerCase() !== "grok") return `${WAKE_MODE_ENV}=${mode}`;
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export type GrokBindApplicability = { applies: true } | { applies: false; reason: string };
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Whether the grok bind (identity lock + switch_profile gate + heal) applies to
|
|
233
|
+
* this process at all.
|
|
234
|
+
*
|
|
235
|
+
* Skipped when:
|
|
236
|
+
* - the process is tagged as a non-Grok wake (see nonGrokWakeReason), or
|
|
237
|
+
* - it was started with an explicit profile (`--profile` / `--name` /
|
|
238
|
+
* AGENTSCHAT_PROFILE) that differs from the profile bound to its
|
|
239
|
+
* CURSOR_CONVERSATION_ID — that operator declared a different bot; an
|
|
240
|
+
* inherited conversation id must not lock it to somebody else.
|
|
241
|
+
*
|
|
242
|
+
* An explicit profile EQUAL to the bound one (Cursor outbound via
|
|
243
|
+
* select-profile-mcp.sh) still applies.
|
|
244
|
+
*/
|
|
245
|
+
export function grokBindApplies(input: {
|
|
246
|
+
env: Record<string, string | undefined>;
|
|
247
|
+
explicitProfileName?: string | null;
|
|
248
|
+
conversationId?: string | undefined;
|
|
249
|
+
binds: Record<string, string>;
|
|
250
|
+
}): GrokBindApplicability {
|
|
251
|
+
const reason = nonGrokWakeReason(input.env);
|
|
252
|
+
if (reason) return { applies: false, reason: `non-grok wake (${reason})` };
|
|
253
|
+
const explicit =
|
|
254
|
+
typeof input.explicitProfileName === "string" ? profileNameFromPath(input.explicitProfileName.trim()) : null;
|
|
255
|
+
if (explicit) {
|
|
256
|
+
const bound = boundProfileForConversation(input.conversationId, input.binds);
|
|
257
|
+
const norm = (n: string) => n.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
258
|
+
if (bound && norm(bound) !== norm(explicit)) {
|
|
259
|
+
return {
|
|
260
|
+
applies: false,
|
|
261
|
+
reason: `explicit profile "${explicit}" differs from grok-bind profile "${bound}"`,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return { applies: true };
|
|
266
|
+
}
|