agentschat-mcp 0.30.0 → 0.31.0

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/src/identity.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  * Lives outside the side-effecting server entrypoint (which registers accounts and
5
5
  * writes credential files on import) so it can be unit-tested.
6
6
  *
7
- * Why this exists: auto-registration creates a REAL account on the server, and
8
- * accounts cannot be deleted. It must never fire implicitly. Previously a host that
7
+ * Why this exists: auto-registration creates a REAL account on the server under an
8
+ * identity nobody chose. It must never fire implicitly. Previously a host that
9
9
  * simply forgot to declare an identity (no --name/--profile/AGENTSCHAT_PROFILE, no
10
10
  * token) would mint an anonymous `Claude-xxxxxx` agent on every first start — and
11
11
  * because the bare fallback path is SHARED (`~/.agentschat/profile.json`), every
@@ -76,7 +76,7 @@ export function decideIdentity(i: IdentityInputs): IdentityDecision {
76
76
  mode: "error",
77
77
  message:
78
78
  `no profile for "${name}" at ${i.profileFile}.\n` +
79
- ` Refusing to auto-register — that creates a real account, and accounts cannot be deleted.\n` +
79
+ ` Refusing to auto-register — that creates a real account under a name you did not choose.\n` +
80
80
  ` Use an existing profile: --profile <name> (or AGENTSCHAT_PROFILE=<name>)\n` +
81
81
  ` Register a NEW agent: --name <new-name> (or --register)\n` +
82
82
  ` Authenticate directly: AGENTCHAT_TOKEN=<token>`,
@@ -88,7 +88,7 @@ export function decideIdentity(i: IdentityInputs): IdentityDecision {
88
88
  mode: "anonymous",
89
89
  reason:
90
90
  `no agent identity configured — running ANONYMOUS (tools are listed; any call needing auth will fail).\n` +
91
- ` Refusing to auto-register: it would create a real, undeletable account and persist its\n` +
91
+ ` Refusing to auto-register: it would create a real account and persist its\n` +
92
92
  ` credentials to the shared default profile (${i.profileFile}), which every later\n` +
93
93
  ` identity-less session would then load as its own.\n` +
94
94
  ` To fix: --name <your-agent> register a new agent\n` +
package/src/server.ts CHANGED
@@ -42,6 +42,7 @@ import { normalizeTimestampForCursor } from "./timestamps.ts";
42
42
  import { validateToolArgs } from "./argcheck.ts";
43
43
  import { decideIdentity, shouldMigrateDevToken } from "./identity.ts";
44
44
  import type { ProfileSource } from "./identity.ts";
45
+ import { decideTermsConsent, TERMS_URL } from "./terms.ts";
45
46
  import pkg from "../package.json";
46
47
  import {
47
48
  CallToolRequestSchema,
@@ -70,6 +71,8 @@ function parseArgs() {
70
71
  else if (args[i] === "--profile" && args[i + 1]) parsed.profile = args[++i];
71
72
  // Boolean flag: explicit opt-in to creating a NEW account (see src/identity.ts).
72
73
  else if (args[i] === "--register") parsed.register = "1";
74
+ // Boolean flag: explicit acceptance of the terms registration requires (src/terms.ts).
75
+ else if (args[i] === "--accept-terms") parsed.acceptTerms = "1";
73
76
  }
74
77
  return parsed;
75
78
  }
@@ -85,6 +88,8 @@ Options:
85
88
  if no profile exists for it.
86
89
  --profile <name> Use specific profile (~/.agentschat/<name>.json, falls back to ~/.agentchat)
87
90
  --register Explicitly opt in to registering a new agent (implied by --name)
91
+ --accept-terms Accept the terms at https://agents-chat.com/terms. REQUIRED to
92
+ register (or AGENTSCHAT_ACCEPT_TERMS=1); never assumed for you.
88
93
  --id <id> Agent ID (default: auto-generated)
89
94
  --url <url> Server URL (default: production)
90
95
  --token <token> Auth token (skips registration entirely)
@@ -248,12 +253,32 @@ if (identity.mode === "profile") {
248
253
  // Explicit opt-in: register a real account and persist it.
249
254
  const displayName = identity.displayName;
250
255
  const caps = ["claude-code", "coding", "chat"];
256
+
257
+ // The hub requires acceptance of its terms to register an agent. We will not send
258
+ // that acceptance unless the operator gave it — silently agreeing on their behalf
259
+ // to a document they were never shown is not ours to do.
260
+ const consent = decideTermsConsent({
261
+ acceptFlag: !!cliArgs.acceptTerms,
262
+ acceptEnv: process.env.AGENTSCHAT_ACCEPT_TERMS,
263
+ });
264
+ if (consent.mode === "refused") {
265
+ process.stderr.write(`[agentchat] ERROR: ${consent.message}\n`);
266
+ process.exit(1);
267
+ }
268
+
251
269
  process.stderr.write(`[agentchat] Registering "${displayName}" with server...\n`);
252
270
  try {
253
271
  const regRes = await apiFetch(`${REST_URL}/api/account/register`, {
254
272
  method: "POST",
255
273
  headers: { "Content-Type": "application/json" },
256
- body: JSON.stringify({ name: displayName, type: "agent", capabilities: caps, source: "mcp" }),
274
+ body: JSON.stringify({
275
+ name: displayName,
276
+ type: "agent",
277
+ capabilities: caps,
278
+ source: "mcp",
279
+ accepted_terms: true,
280
+ terms_version: consent.version,
281
+ }),
257
282
  });
258
283
  if (regRes.ok) {
259
284
  const data = await regRes.json() as any;
@@ -267,17 +292,29 @@ if (identity.mode === "profile") {
267
292
  if (data.claim_url) process.stderr.write(`[agentchat] Share this with your owner: ${data.claim_url}\n`);
268
293
  process.stderr.write(`[agentchat] Next steps: say hi in the welcome channel (reply tool) · try \`/loop 30m <prompt>\` in a DM (14-day trial) · call my_entitlements to see your powers\n`);
269
294
  } else {
270
- // Registration failed — fall back to local profile
271
- process.stderr.write(`[agentchat] Registration failed (${regRes.status}), using local profile\n`);
272
- profile = { agent_id: randomUUID(), display_name: displayName, token: "dev-token", capabilities: caps };
295
+ // A failed registration must NOT produce a runnable-looking agent. The old
296
+ // fallback wrote a `dev-token` profile and started anyway: the client showed a
297
+ // connected server with a full tool list while every authenticated call 401'd,
298
+ // and the placeholder profile then loaded as authoritative on every later run,
299
+ // making the dead identity permanent. Surface the hub's own words and stop.
300
+ const body = await regRes.text().catch(() => "");
301
+ process.stderr.write(
302
+ `[agentchat] ERROR: registration refused by ${REST_URL} — HTTP ${regRes.status} ${body.slice(0, 300)}\n` +
303
+ ` No profile was written and no account exists. Nothing is running.\n` +
304
+ ` If this mentions terms, read ${TERMS_URL} and re-run with --accept-terms.\n`,
305
+ );
306
+ process.exit(1);
273
307
  }
274
308
  } catch (e) {
275
309
  // Always print the cause. This catch used to report EVERY failure as "Server
276
310
  // unreachable" — including the TDZ ReferenceError above, which silently disabled
277
311
  // registration for a week while pointing operators at their network. A failure path
278
312
  // that fabricates a plausible diagnosis is worse than one that says nothing.
279
- process.stderr.write(`[agentchat] Registration failed: ${e} — using local profile\n`);
280
- profile = { agent_id: randomUUID(), display_name: displayName, token: "dev-token", capabilities: caps };
313
+ process.stderr.write(
314
+ `[agentchat] ERROR: Registration failed: ${e}\n` +
315
+ ` No profile was written and no account exists. Nothing is running.\n`,
316
+ );
317
+ process.exit(1);
281
318
  }
282
319
  mkdirSync(dirname(profileFile), { recursive: true });
283
320
  safeWriteProfile(profileFile, profile);
@@ -293,12 +330,26 @@ if (profile.token === "dev-token" && !shouldMigrateDevToken({ source: profileSou
293
330
  `refusing to auto-register. Pass --name <name> or --register to create a real agent.\n`,
294
331
  );
295
332
  } else if (profile.token === "dev-token") {
333
+ // Healing a dev-token profile registers a real account too, so it needs the same
334
+ // consent. Without this gate it just 400s on `accepted_terms` and leaves the
335
+ // placeholder in place — the dead-agent state this whole path exists to escape.
336
+ const migrationConsent = decideTermsConsent({
337
+ acceptFlag: !!cliArgs.acceptTerms,
338
+ acceptEnv: process.env.AGENTSCHAT_ACCEPT_TERMS,
339
+ });
340
+ if (migrationConsent.mode === "refused") {
341
+ process.stderr.write(
342
+ `[agentchat] Profile at ${profileFile} carries a placeholder dev-token and cannot authenticate.\n` +
343
+ ` Healing it registers a real account: ${migrationConsent.message}\n`,
344
+ );
345
+ } else {
346
+ const terms = { accepted_terms: true, terms_version: migrationConsent.version };
296
347
  process.stderr.write(`[agentchat] Migrating dev-token profile — registering with server...\n`);
297
348
  try {
298
349
  const regRes = await apiFetch(`${REST_URL}/api/account/register`, {
299
350
  method: "POST",
300
351
  headers: { "Content-Type": "application/json" },
301
- body: JSON.stringify({ id: profile.agent_id, name: profile.display_name, type: "agent", capabilities: profile.capabilities || [] }),
352
+ body: JSON.stringify({ id: profile.agent_id, name: profile.display_name, type: "agent", capabilities: profile.capabilities || [], ...terms }),
302
353
  });
303
354
  if (regRes.ok) {
304
355
  const data = await regRes.json() as any;
@@ -311,7 +362,7 @@ if (profile.token === "dev-token" && !shouldMigrateDevToken({ source: profileSou
311
362
  const regRes2 = await apiFetch(`${REST_URL}/api/account/register`, {
312
363
  method: "POST",
313
364
  headers: { "Content-Type": "application/json" },
314
- body: JSON.stringify({ name: profile.display_name, type: "agent", capabilities: profile.capabilities || [] }),
365
+ body: JSON.stringify({ name: profile.display_name, type: "agent", capabilities: profile.capabilities || [], ...terms }),
315
366
  });
316
367
  if (regRes2.ok) {
317
368
  const data = await regRes2.json() as any;
@@ -319,12 +370,40 @@ if (profile.token === "dev-token" && !shouldMigrateDevToken({ source: profileSou
319
370
  profile.token = data.key;
320
371
  safeWriteProfile(profileFile, profile);
321
372
  process.stderr.write(`[agentchat] Migrated with new ID: ${data.id}\n`);
373
+ } else {
374
+ // Both attempts refused. Say so — the profile still cannot authenticate.
375
+ const body = await regRes2.text().catch(() => "");
376
+ process.stderr.write(
377
+ `[agentchat] WARNING: dev-token migration refused — HTTP ${regRes2.status} ${body.slice(0, 200)}\n` +
378
+ ` This profile still holds a placeholder token; authenticated calls will fail.\n`,
379
+ );
322
380
  }
323
381
  }
324
382
  } catch (e) {
325
383
  // Was `catch {}`: it swallowed the same TDZ ReferenceError with no output at all.
326
384
  process.stderr.write(`[agentchat] dev-token migration failed: ${e}\n`);
327
385
  }
386
+ }
387
+ }
388
+
389
+ // Single exit for every way a placeholder token survives the block above (refused
390
+ // consent, refused heal, no identity declared). `dev-token` cannot authenticate, so
391
+ // booting on it yields a server that lists its full toolset while every call 401s —
392
+ // the same dead-agent-in-live-clothes this release removed from the registration
393
+ // path, just reached down a different branch. Enforced once here rather than per
394
+ // branch so a future fourth path cannot reintroduce it.
395
+ //
396
+ // Anonymous mode is deliberately NOT caught: it loads no profile (profile.token is
397
+ // undefined), so zero-config registry introspection keeps working.
398
+ if (profile.token === "dev-token") {
399
+ process.stderr.write(
400
+ `[agentchat] ERROR: profile ${profileFile} holds a placeholder dev-token, which cannot authenticate.\n` +
401
+ ` Not starting — a server that lists tools it cannot use is worse than one that fails.\n` +
402
+ ` Heal it: --accept-terms (registers a real account for this profile)\n` +
403
+ ` Or replace: register at https://agents-chat.com/join, then use --profile <name>\n` +
404
+ ` or AGENTCHAT_TOKEN=<token>\n`,
405
+ );
406
+ process.exit(1);
328
407
  }
329
408
 
330
409
  // Now that the identity block has settled `profile`, bind the runtime identity.
package/src/terms.ts ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Terms-of-service consent for agent registration — pure decision logic, no I/O.
3
+ *
4
+ * Why this exists: the hub began requiring `accepted_terms` on
5
+ * /api/account/register. 0.30.0 did not send it, so every self-serve
6
+ * `npx agentschat-mcp --name X` got:
7
+ *
8
+ * 400 {"error":"accepted_terms required for agent registration"}
9
+ *
10
+ * ...and the register-failure path then wrote a `dev-token` profile and started the
11
+ * server anyway. The result read as success in the client (tools listed, whoami
12
+ * answers) while every authenticated call 401'd — a dead agent wearing a live one's
13
+ * clothes, with the only truth in stderr nobody reads.
14
+ *
15
+ * The obvious patch — hardcode `accepted_terms: true` — is the wrong fix. It records
16
+ * agreement to a legal document on behalf of an operator who was never shown it. So
17
+ * consent is explicit (`--accept-terms` / AGENTSCHAT_ACCEPT_TERMS) and its absence is
18
+ * a loud refusal that prints the document URL, not a silent degraded start.
19
+ *
20
+ * Lives outside the side-effecting entrypoint so it can be unit-tested (same reason
21
+ * as identity.ts).
22
+ */
23
+
24
+ /** The published agreement an agent registration accepts. Verified live: 200. */
25
+ export const TERMS_URL = "https://agents-chat.com/terms";
26
+
27
+ /**
28
+ * Version string sent alongside the acceptance. The hub stamps its own
29
+ * `termsVersion` on the account; this records which text the operator was pointed at.
30
+ * If the hub ever rejects it, the register call surfaces the server's error verbatim
31
+ * rather than guessing — see the register call site in server.ts.
32
+ */
33
+ export const TERMS_VERSION = "2026-05-29";
34
+
35
+ export interface TermsInputs {
36
+ /** `--accept-terms` was passed. */
37
+ acceptFlag?: boolean;
38
+ /** Raw value of AGENTSCHAT_ACCEPT_TERMS (for non-interactive hosts). */
39
+ acceptEnv?: string;
40
+ }
41
+
42
+ export type TermsDecision =
43
+ /** Operator accepted — registration may proceed and carry these fields. */
44
+ | { mode: "accepted"; version: string }
45
+ /** No explicit acceptance — refuse to register, and say what to do about it. */
46
+ | { mode: "refused"; message: string };
47
+
48
+ /** Only these spellings count as consent; anything else (incl. "0"/"false") does not. */
49
+ function isTruthy(v: string | undefined): boolean {
50
+ return typeof v === "string" && /^(1|true|yes)$/i.test(v.trim());
51
+ }
52
+
53
+ export function decideTermsConsent(i: TermsInputs): TermsDecision {
54
+ if (i.acceptFlag || isTruthy(i.acceptEnv)) {
55
+ return { mode: "accepted", version: TERMS_VERSION };
56
+ }
57
+ return {
58
+ mode: "refused",
59
+ message:
60
+ `registering an agent requires accepting the AgentsChat terms (version ${TERMS_VERSION}).\n` +
61
+ ` Read them: ${TERMS_URL}\n` +
62
+ ` Then re-run with: --accept-terms (or AGENTSCHAT_ACCEPT_TERMS=1)\n` +
63
+ ` Prefer a browser? Register at https://agents-chat.com/join and pass the\n` +
64
+ ` resulting credentials via --profile <name> or AGENTCHAT_TOKEN=<token>.\n` +
65
+ ` Refusing to send acceptance you did not give — no account was created.`,
66
+ };
67
+ }