rogerthat 1.21.2 → 1.24.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/dist/app.js +105 -7
- package/dist/channel.js +34 -12
- package/dist/discovery.js +102 -3
- package/dist/landing.js +250 -3
- package/dist/listen-here.js +6 -0
- package/dist/mcp.js +250 -23
- package/dist/presets.js +17 -0
- package/dist/remote-control.js +52 -1
- package/dist/remote-ui.js +56 -0
- package/dist/store.js +54 -0
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -15,14 +15,14 @@ import { getOrCreateChannel, listActiveChannels } from "./channel.js";
|
|
|
15
15
|
import { buildConnectInfo } from "./connect.js";
|
|
16
16
|
import { agentCard } from "./agentcard.js";
|
|
17
17
|
import { llmsText, mcpDescriptor, serviceInfo } from "./discovery.js";
|
|
18
|
-
import { landingHtml } from "./landing.js";
|
|
18
|
+
import { landingHtml, phoneLandingHtml } from "./landing.js";
|
|
19
19
|
import { handleMcpRequest } from "./mcp.js";
|
|
20
20
|
import { remoteHtml } from "./remote-ui.js";
|
|
21
|
-
import { createRemoteControl } from "./remote-control.js";
|
|
21
|
+
import { createRemoteControl, retrofitRemoteLink } from "./remote-control.js";
|
|
22
22
|
import { policyHtml, policyText } from "./policy.js";
|
|
23
23
|
import { applyPresetDefaults, getPreset, resolveMode, } from "./presets.js";
|
|
24
24
|
import { recordJoin as statsRecordJoin, recordMessage as statsRecordMessage, getStats, } from "./stats.js";
|
|
25
|
-
import { channelExists, createChannel, deleteChannelByCreator, ensureBands, getChannelIsBand, getChannelRequireIdentity, getChannelRetention, getChannelSessionTtlMs, getChannelTrustMode, hasOwnerPassword, listBands, listChannelsByCreator, verifyChannel, verifyOwnerPassword, } from "./store.js";
|
|
25
|
+
import { channelExists, createChannel, deleteChannelByCreator, setSessionTtlByCreator, ensureBands, getChannelIsBand, getChannelRequireIdentity, getChannelRetention, getChannelSessionTtlMs, getChannelTrustMode, hasOwnerPassword, listBands, listChannelsByCreator, verifyChannel, verifyOwnerPassword, } from "./store.js";
|
|
26
26
|
import { isRetention, readTranscript, recordJoin as transcriptRecordJoin, recordLeave as transcriptRecordLeave, recordMessage as transcriptRecordMessage, } from "./transcripts.js";
|
|
27
27
|
export function createApp(opts) {
|
|
28
28
|
ensureBands();
|
|
@@ -72,6 +72,9 @@ export function createApp(opts) {
|
|
|
72
72
|
if (accept.includes("application/json") && !accept.includes("text/html")) {
|
|
73
73
|
return c.json(serviceInfo(opts.publicOrigin));
|
|
74
74
|
}
|
|
75
|
+
const mode = c.get("mode") ?? "default";
|
|
76
|
+
if (mode === "phone")
|
|
77
|
+
return c.html(phoneLandingHtml());
|
|
75
78
|
return c.html(landingHtml());
|
|
76
79
|
});
|
|
77
80
|
app.get("/healthz", (c) => c.text("ok"));
|
|
@@ -444,6 +447,52 @@ export function createApp(opts) {
|
|
|
444
447
|
notice: "Two-step phone flow: (1) open mobile_url on the phone; (2) type owner_password on the /remote setup screen to mark the phone session as human-authorized. The password is NOT embedded in mobile_url on purpose — relay it through a separate channel so a leaked URL alone can't impersonate the human. On the agent side (this machine), join with agent.identity_key + owner_password and then loop on `wait`.",
|
|
445
448
|
});
|
|
446
449
|
});
|
|
450
|
+
// Retrofit a phone-control link onto an EXISTING channel. Use when the
|
|
451
|
+
// operator originally created a plain channel and the human shows up later
|
|
452
|
+
// wanting to drive from a phone — instead of forcing a new channel +
|
|
453
|
+
// migrating all the agents, this mints a phone identity + (if not already
|
|
454
|
+
// set) an owner_password, and returns a mobile_url/QR for the same channel.
|
|
455
|
+
app.post("/api/channels/:id/remote-link", async (c) => {
|
|
456
|
+
const channelId = c.req.param("id") ?? "";
|
|
457
|
+
if (!channelId)
|
|
458
|
+
return c.json({ error: "channel_id required" }, 400);
|
|
459
|
+
let body = {};
|
|
460
|
+
try {
|
|
461
|
+
const raw = c.req.header("content-type")?.startsWith("application/json")
|
|
462
|
+
? await c.req.json()
|
|
463
|
+
: {};
|
|
464
|
+
if (raw && typeof raw === "object")
|
|
465
|
+
body = raw;
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
/* body optional */
|
|
469
|
+
}
|
|
470
|
+
const channelToken = typeof body.channel_token === "string" ? body.channel_token : "";
|
|
471
|
+
const sessionToken = typeof body.session_token === "string" ? body.session_token : "";
|
|
472
|
+
if (!channelToken)
|
|
473
|
+
return c.json({ error: "channel_token required" }, 400);
|
|
474
|
+
if (!sessionToken)
|
|
475
|
+
return c.json({ error: "session_token required (account must own a phone identity)" }, 400);
|
|
476
|
+
const result = await retrofitRemoteLink({
|
|
477
|
+
publicOrigin: opts.publicOrigin,
|
|
478
|
+
channelId,
|
|
479
|
+
channelToken,
|
|
480
|
+
sessionToken,
|
|
481
|
+
});
|
|
482
|
+
if ("error" in result) {
|
|
483
|
+
const status = result.code === "unauthorized" ? 401 :
|
|
484
|
+
result.code === "not_found" ? 404 :
|
|
485
|
+
result.code === "bad_token" ? 403 :
|
|
486
|
+
500;
|
|
487
|
+
return c.json({ error: result.error }, status);
|
|
488
|
+
}
|
|
489
|
+
return c.json({
|
|
490
|
+
...result,
|
|
491
|
+
notice: result.owner_password_existing
|
|
492
|
+
? "This channel already had an owner_password set — we did NOT rotate it (would invalidate every peer that already joined with it). Use the password you already have OOB; the mobile_url above + that password = phone joins as human-authorized."
|
|
493
|
+
: "Two-step phone flow: (1) open mobile_url on the phone; (2) type owner_password on the /remote setup screen. The password is NOT embedded in mobile_url on purpose — relay it through a separate channel so a leaked URL alone can't impersonate the human.",
|
|
494
|
+
});
|
|
495
|
+
});
|
|
447
496
|
app.delete("/api/account/channels/:id", (c) => {
|
|
448
497
|
const r = requireSession(c);
|
|
449
498
|
if (r instanceof Response)
|
|
@@ -454,6 +503,45 @@ export function createApp(opts) {
|
|
|
454
503
|
return c.json({ error: "channel not found or not yours" }, 404);
|
|
455
504
|
return c.json({ ok: true });
|
|
456
505
|
});
|
|
506
|
+
// Mutate the idle session TTL on an existing channel. Owner-only — same gate
|
|
507
|
+
// as DELETE. Use case: an agent started a 30-min channel for a quick task,
|
|
508
|
+
// the conversation turned into a 4-hour debugging session, and the operator
|
|
509
|
+
// wants to push TTL out to 24h instead of dealing with re-joins. Companion
|
|
510
|
+
// to /api/channels/:id/remote-link — same "retrofit instead of recreate"
|
|
511
|
+
// pattern.
|
|
512
|
+
app.patch("/api/account/channels/:id/session-ttl", async (c) => {
|
|
513
|
+
const r = requireSession(c);
|
|
514
|
+
if (r instanceof Response)
|
|
515
|
+
return r;
|
|
516
|
+
const channelId = c.req.param("id") ?? "";
|
|
517
|
+
if (!channelId)
|
|
518
|
+
return c.json({ error: "channel_id required" }, 400);
|
|
519
|
+
let body = {};
|
|
520
|
+
try {
|
|
521
|
+
const raw = c.req.header("content-type")?.startsWith("application/json")
|
|
522
|
+
? await c.req.json()
|
|
523
|
+
: {};
|
|
524
|
+
if (raw && typeof raw === "object")
|
|
525
|
+
body = raw;
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
/* body optional */
|
|
529
|
+
}
|
|
530
|
+
const ttl = typeof body.session_ttl_seconds === "number" ? body.session_ttl_seconds : NaN;
|
|
531
|
+
const result = setSessionTtlByCreator(r.accountId, channelId, ttl);
|
|
532
|
+
if ("error" in result) {
|
|
533
|
+
const status = result.code === "not_found" ? 404 :
|
|
534
|
+
result.code === "forbidden" ? 403 :
|
|
535
|
+
400;
|
|
536
|
+
return c.json({ error: result.error }, status);
|
|
537
|
+
}
|
|
538
|
+
return c.json({
|
|
539
|
+
ok: true,
|
|
540
|
+
channel_id: channelId,
|
|
541
|
+
session_ttl_seconds: result.session_ttl_seconds,
|
|
542
|
+
notice: "New TTL applies to the next GC tick (within 60s). Sessions already past the previous TTL but not yet evicted are rescued by a bump; idle sessions inside the previous TTL but outside the new one will be evicted sooner if you shrank it.",
|
|
543
|
+
});
|
|
544
|
+
});
|
|
457
545
|
// ─── Webhooks ───
|
|
458
546
|
app.post("/api/account/webhooks", async (c) => {
|
|
459
547
|
const r = requireSession(c);
|
|
@@ -754,6 +842,12 @@ export function createApp(opts) {
|
|
|
754
842
|
if (priorityInput !== undefined && !isPriority(priorityInput)) {
|
|
755
843
|
return c.json({ error: "invalid priority; must be one of min|low|default|high|urgent", code: "invalid" }, 400);
|
|
756
844
|
}
|
|
845
|
+
// Optional message kind. 'status' = ephemeral working/typing signal.
|
|
846
|
+
const kindInput = body.kind;
|
|
847
|
+
if (kindInput !== undefined && kindInput !== "message" && kindInput !== "status") {
|
|
848
|
+
return c.json({ error: "invalid kind; must be 'message' or 'status'", code: "invalid" }, 400);
|
|
849
|
+
}
|
|
850
|
+
const kind = kindInput === "status" ? "status" : undefined;
|
|
757
851
|
let suggestedReplies;
|
|
758
852
|
let attachments;
|
|
759
853
|
try {
|
|
@@ -775,17 +869,21 @@ export function createApp(opts) {
|
|
|
775
869
|
retry_after_seconds: rate.retryAfter,
|
|
776
870
|
}, 429);
|
|
777
871
|
}
|
|
778
|
-
const msg = channel.send(sessionId, to, message, priorityInput, suggestedReplies, attachments);
|
|
872
|
+
const msg = channel.send(sessionId, to, message, priorityInput, suggestedReplies, attachments, kind);
|
|
779
873
|
statsRecordMessage();
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
874
|
+
// Status pings are ephemeral — keep them out of transcripts and webhooks.
|
|
875
|
+
if (msg.kind !== "status") {
|
|
876
|
+
transcriptRecordMessage(channelId, getChannelRetention(channelId), msg);
|
|
877
|
+
fanoutWebhooks(channelId, msg);
|
|
878
|
+
}
|
|
879
|
+
const queued = msg.kind !== "status" && msg.to !== "all" && !channel.isCallsignOnline(msg.to);
|
|
783
880
|
return c.json({
|
|
784
881
|
ok: true,
|
|
785
882
|
id: msg.id,
|
|
786
883
|
at: msg.at,
|
|
787
884
|
queued,
|
|
788
885
|
to: msg.to,
|
|
886
|
+
...(msg.kind ? { kind: msg.kind } : {}),
|
|
789
887
|
...(msg.priority ? { priority: msg.priority } : {}),
|
|
790
888
|
...(msg.suggested_replies ? { suggested_replies: msg.suggested_replies } : {}),
|
|
791
889
|
});
|
package/dist/channel.js
CHANGED
|
@@ -302,7 +302,7 @@ export class Channel {
|
|
|
302
302
|
sessionExists(sessionId) {
|
|
303
303
|
return this.callsignBySession.has(sessionId);
|
|
304
304
|
}
|
|
305
|
-
send(sessionId, to, text, priority, suggestedReplies, attachments) {
|
|
305
|
+
send(sessionId, to, text, priority, suggestedReplies, attachments, kind) {
|
|
306
306
|
this.ensureJoined(sessionId);
|
|
307
307
|
const from = this.callsignBySession.get(sessionId);
|
|
308
308
|
// Empty/missing `to` defaults to broadcast. Walkie-talkie physical default —
|
|
@@ -315,13 +315,25 @@ export class Channel {
|
|
|
315
315
|
if (typeof text !== "string") {
|
|
316
316
|
throw new ChannelError("message text must be a string", "invalid", 400);
|
|
317
317
|
}
|
|
318
|
-
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
318
|
+
const isStatus = kind === "status";
|
|
319
|
+
// A status signal is a note (e.g. "on it, ~1 min") — it always needs text,
|
|
320
|
+
// and attachments / suggested_replies don't apply. Normal messages allow
|
|
321
|
+
// empty text only when an attachment carries the payload.
|
|
322
|
+
if (isStatus) {
|
|
323
|
+
if (text.length === 0) {
|
|
324
|
+
throw new ChannelError("status message requires text (e.g. 'received, working on it')", "invalid", 400);
|
|
325
|
+
}
|
|
326
|
+
if (text.length > 280) {
|
|
327
|
+
throw new ChannelError("status message too long (max 280 chars — it's a short note, not content)", "invalid", 400);
|
|
328
|
+
}
|
|
322
329
|
}
|
|
323
|
-
|
|
324
|
-
|
|
330
|
+
else {
|
|
331
|
+
if (text.length === 0 && (!attachments || attachments.length === 0)) {
|
|
332
|
+
throw new ChannelError("message text required (or send at least one attachment)", "invalid", 400);
|
|
333
|
+
}
|
|
334
|
+
if (text.length > 8192) {
|
|
335
|
+
throw new ChannelError("message too long (max 8192 chars)", "invalid", 400);
|
|
336
|
+
}
|
|
325
337
|
}
|
|
326
338
|
this.touch(sessionId);
|
|
327
339
|
// Strictly-monotonic timestamp ID: at least one millisecond ahead of the prior id, and at
|
|
@@ -329,19 +341,29 @@ export class Channel {
|
|
|
329
341
|
const now = Date.now();
|
|
330
342
|
this.nextMsgId = Math.max(now, this.nextMsgId + 1);
|
|
331
343
|
const msg = { id: this.nextMsgId, from, to: dest, text, at: now };
|
|
344
|
+
if (isStatus)
|
|
345
|
+
msg.kind = "status";
|
|
332
346
|
// Only attach `priority` when explicitly non-default — keeps the wire format
|
|
333
347
|
// backward-compatible for consumers that don't know about priorities.
|
|
334
348
|
if (priority && priority !== "default")
|
|
335
349
|
msg.priority = priority;
|
|
336
|
-
|
|
350
|
+
// suggested_replies + attachments are content-message features — a status
|
|
351
|
+
// ping is a bare note, so they're dropped even if passed.
|
|
352
|
+
if (!isStatus && suggestedReplies && suggestedReplies.length > 0) {
|
|
337
353
|
msg.suggested_replies = suggestedReplies;
|
|
338
354
|
}
|
|
339
|
-
if (attachments && attachments.length > 0) {
|
|
355
|
+
if (!isStatus && attachments && attachments.length > 0) {
|
|
340
356
|
msg.attachments = attachments;
|
|
341
357
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
358
|
+
// Status signals are ephemeral: delivered to whoever is listening right
|
|
359
|
+
// now, but NOT stored — they never enter the ring buffer, so history()
|
|
360
|
+
// stays clean and an offline peer simply misses the "working" note
|
|
361
|
+
// (by the time it reconnects, the real reply is what matters).
|
|
362
|
+
if (!isStatus) {
|
|
363
|
+
this.messages.push(msg);
|
|
364
|
+
if (this.messages.length > HISTORY_CAP)
|
|
365
|
+
this.messages.shift();
|
|
366
|
+
}
|
|
345
367
|
this.notify(msg);
|
|
346
368
|
return msg;
|
|
347
369
|
}
|
package/dist/discovery.js
CHANGED
|
@@ -22,6 +22,56 @@ function modeBanner(mode, origin) {
|
|
|
22
22
|
const preset = getPreset(mode);
|
|
23
23
|
if (!preset)
|
|
24
24
|
return "";
|
|
25
|
+
// Phone mode is structurally different: there's only ONE entry point worth
|
|
26
|
+
// mentioning (open_remote_control / POST /api/remote-control), and the
|
|
27
|
+
// operator should not even be in the create_channel conversation. We render
|
|
28
|
+
// a bespoke banner that lists that one path and skips the trust/retention
|
|
29
|
+
// bullets — they're irrelevant noise at this front door.
|
|
30
|
+
if (mode === "phone") {
|
|
31
|
+
return [
|
|
32
|
+
`# PHONE MODE — ${preset.tagline}`,
|
|
33
|
+
"",
|
|
34
|
+
"You reached this page via `phone.rogerthat.chat`. **The URL is the selection.** On this subdomain there is exactly ONE path — do not call `create_channel`, do not ask the operator about trust/retention/identity/TTL.",
|
|
35
|
+
"",
|
|
36
|
+
"## The only call you need on this subdomain",
|
|
37
|
+
"",
|
|
38
|
+
"**MCP (if `rogerthat` is already configured):**",
|
|
39
|
+
"",
|
|
40
|
+
"```",
|
|
41
|
+
"open_remote_control()",
|
|
42
|
+
"```",
|
|
43
|
+
"",
|
|
44
|
+
"**No MCP (works in any shell-capable agent):**",
|
|
45
|
+
"",
|
|
46
|
+
"```bash",
|
|
47
|
+
`curl -sX POST ${origin}/api/remote-control -H 'Content-Type: application/json' -d '{}'`,
|
|
48
|
+
"```",
|
|
49
|
+
"",
|
|
50
|
+
"Response fields (same for both transports):",
|
|
51
|
+
"",
|
|
52
|
+
"- `mobile_url` — open this on the phone; auto-prompts for the password.",
|
|
53
|
+
"- `owner_password` — show this to the human; they type it on the phone screen.",
|
|
54
|
+
"- `qr_ascii` — render in the terminal so the operator can scan from their phone instead of copy-pasting a long URL.",
|
|
55
|
+
"- `channel_id`, `channel_token`, `agent.callsign`, `agent.identity_key` — what YOU use to join.",
|
|
56
|
+
"- `receiver_command_template`, `monitor_command_template`, `selftest_command_template` — pre-formed Bash + Monitor commands. Run them in that order; the selftest confirms the wiring before the human sends anything.",
|
|
57
|
+
"",
|
|
58
|
+
"## After the bootstrap",
|
|
59
|
+
"",
|
|
60
|
+
"1. `join` with `channel_id` + `channel_token` + `agent.identity_key` + `owner_password` — get back `session_id`.",
|
|
61
|
+
"2. Run `receiver_command_template` detached in your Bash tool (substitute `<SID>` with the session_id).",
|
|
62
|
+
"3. Paste `monitor_command_template` LITERALLY into your Monitor tool. One new line in the inbox = one notification.",
|
|
63
|
+
"4. Run `selftest_command_template` — your Monitor fires once with a `[selftest]` line. That proves file path + Monitor wiring are correct even while the listener is still warming up (first `npx -y rogerthat` takes 30-60s).",
|
|
64
|
+
"5. **Only after the selftest notification arrives**, `send` to:'all' a one-line greeting (no `kind`) so the human sees you're alive when they open the mobile URL.",
|
|
65
|
+
"6. For any request that will take more than a few seconds, fire a `send` with `kind:'status'` first — the phone renders that as a transient `● working…` indicator.",
|
|
66
|
+
"",
|
|
67
|
+
preset.narrative,
|
|
68
|
+
"",
|
|
69
|
+
`Anything not covered above? The canonical unfiltered guide is at ${origin}/llms.txt — same server, same backend, just rendered without the mode filter.`,
|
|
70
|
+
"",
|
|
71
|
+
"---",
|
|
72
|
+
"",
|
|
73
|
+
].join("\n");
|
|
74
|
+
}
|
|
25
75
|
const recommendedReceiveBlock = preset.recommendedReceive === "polling"
|
|
26
76
|
? `**Recommended receive method for this mode: tight long-polling against \`/listen\`.** Both sides of this conversation are active in turn, so polling is cheap and zero-setup. listen-here is overkill; webhooks add latency.`
|
|
27
77
|
: preset.recommendedReceive === "webhook"
|
|
@@ -422,6 +472,27 @@ Once on a channel, \`roster()\` returns agents with their join-order index. You
|
|
|
422
472
|
|
|
423
473
|
So if the user tells you *"hablale al agente #12 en rogerthat"*, that maps cleanly.
|
|
424
474
|
|
|
475
|
+
## Status signals — show the peer you're working
|
|
476
|
+
|
|
477
|
+
Agent replies are often slow: you receive a request, then spend 30 s–2 min on a build, a search, or a multi-step task before you can answer. The peer (and any human watching the /remote phone view) just sees silence and can't tell if you got the message.
|
|
478
|
+
|
|
479
|
+
Fix: the moment you pick up a request that will take more than a few seconds, send a **status signal** before you start working:
|
|
480
|
+
|
|
481
|
+
\`\`\`bash
|
|
482
|
+
# MCP: send with kind="status"
|
|
483
|
+
send({ to: "all", message: "received — running the build, ~1 min", kind: "status" })
|
|
484
|
+
|
|
485
|
+
# REST: add "kind":"status" to the body
|
|
486
|
+
curl -s -X POST ${origin}/api/channels/$CHID/send \\
|
|
487
|
+
-H "Authorization: Bearer $TOKEN" -H "X-Session-Id: $SID" \\
|
|
488
|
+
-H 'Content-Type: application/json' \\
|
|
489
|
+
-d '{"to":"all","message":"received — on it, ~1 min","kind":"status"}'
|
|
490
|
+
\`\`\`
|
|
491
|
+
|
|
492
|
+
Then do the work, then send your real answer as a **normal** message (no \`kind\`).
|
|
493
|
+
|
|
494
|
+
Status signals are **ephemeral**: they reach whoever is listening right now, but are NOT stored — they never show up in \`history()\`, and a peer who was offline simply never sees them. The /remote UI renders them as a transient "● working…" indicator that the next real message clears. Keep the note short (≤280 chars). This is the recommended courtesy on every channel — it turns dead silence into a visible loading state.
|
|
495
|
+
|
|
425
496
|
## Communication policy
|
|
426
497
|
|
|
427
498
|
Before behaving on a channel, **read ${origin}/policy.txt** (markdown) or ${origin}/policy (HTML). The policy covers:
|
|
@@ -459,9 +530,9 @@ export function mcpDescriptor(origin) {
|
|
|
459
530
|
{
|
|
460
531
|
type: "http",
|
|
461
532
|
url: `${origin}/mcp`,
|
|
462
|
-
description: "Unified MCP endpoint. Single install per machine — all tools available. Use the 'join' tool with channel_id+token+callsign args to enter any channel from the same session.
|
|
533
|
+
description: "Unified MCP endpoint. Single install per machine — all tools available. Use the 'join' tool with channel_id+token+callsign args to enter any channel from the same session. 'open_remote_control' bootstraps a phone-to-agent control channel in one call from scratch; 'make_remote_link' retrofits a phone link onto an EXISTING channel (no migration needed); 'update_channel_ttl' bumps or shrinks the idle TTL on a channel you created. Recommended.",
|
|
463
534
|
auth: "none for create_channel and discovery; token passed in join's args",
|
|
464
|
-
tools: ["create_channel", "join", "send", "listen", "wait", "roster", "history", "leave", "open_remote_control", "create_account", "create_identity"],
|
|
535
|
+
tools: ["create_channel", "join", "send", "listen", "wait", "roster", "history", "leave", "open_remote_control", "make_remote_link", "update_channel_ttl", "create_account", "create_identity"],
|
|
465
536
|
},
|
|
466
537
|
{
|
|
467
538
|
type: "http",
|
|
@@ -475,7 +546,7 @@ export function mcpDescriptor(origin) {
|
|
|
475
546
|
note: "Full equivalent of the MCP tool surface — usable by any CLI with shell/curl access. No MCP install needed.",
|
|
476
547
|
create_channel: { method: "POST", path: "/api/channels", body: { retention: "none|metadata|prompts|full" } },
|
|
477
548
|
join: { method: "POST", path: "/api/channels/{id}/join", auth: "Bearer", body: { callsign: "string" }, returns: { session_id: "string", roster: [], history: [] } },
|
|
478
|
-
send: { method: "POST", path: "/api/channels/{id}/send", auth: "Bearer + X-Session-Id", body: { to: "callsign or 'all'", message: "string" } },
|
|
549
|
+
send: { method: "POST", path: "/api/channels/{id}/send", auth: "Bearer + X-Session-Id", body: { to: "callsign or 'all'", message: "string", kind: "optional 'message'|'status' — 'status' = ephemeral working signal" } },
|
|
479
550
|
listen: { method: "GET", path: "/api/channels/{id}/listen?timeout=N", auth: "Bearer + X-Session-Id", notes: "long-polls up to 60s" },
|
|
480
551
|
roster: { method: "GET", path: "/api/channels/{id}/roster", auth: "Bearer" },
|
|
481
552
|
history: { method: "GET", path: "/api/channels/{id}/history?n=N", auth: "Bearer" },
|
|
@@ -500,6 +571,34 @@ export function mcpDescriptor(origin) {
|
|
|
500
571
|
},
|
|
501
572
|
notes: "Bootstrap for 'drive my agent from my phone'. Mints a private trusted channel + two identities. The agent on the original machine joins with agent.identity_key + owner_password (→ trusted-authorized). The human opens mobile_url on any device and types owner_password to join as human-authorized. The password is delivered OOB by design — leaking the URL alone doesn't authorize the leaker.",
|
|
502
573
|
},
|
|
574
|
+
remote_link: {
|
|
575
|
+
method: "POST",
|
|
576
|
+
path: "/api/channels/{id}/remote-link",
|
|
577
|
+
auth: "channel_token + session_token (both in body)",
|
|
578
|
+
body: { channel_token: "string", session_token: "string" },
|
|
579
|
+
returns: {
|
|
580
|
+
channel_id: "string",
|
|
581
|
+
channel_token: "string",
|
|
582
|
+
owner_password: "string|null — newly minted password, OR null if the channel already had one (we don't rotate it)",
|
|
583
|
+
owner_password_existing: "boolean — true means caller must use the password they already have OOB",
|
|
584
|
+
phone: { callsign: "string", identity_key: "string" },
|
|
585
|
+
mobile_url: "string",
|
|
586
|
+
qr_ascii: "string — terminal-renderable QR of mobile_url",
|
|
587
|
+
},
|
|
588
|
+
notes: "Retrofit a phone-control link onto an EXISTING channel. Use when agents are already joined and the human shows up wanting to drive from a phone — no need to migrate to a fresh channel. Mints a phone identity on the caller's account. trust_mode / require_identity / session_ttl are NOT mutated; if the channel had no owner_password we mint one; if it already had one we leave it alone and signal owner_password_existing=true.",
|
|
589
|
+
},
|
|
590
|
+
update_channel_ttl: {
|
|
591
|
+
method: "PATCH",
|
|
592
|
+
path: "/api/account/channels/{id}/session-ttl",
|
|
593
|
+
auth: "Bearer session_token (channel owner only)",
|
|
594
|
+
body: { session_ttl_seconds: "1-86400 integer" },
|
|
595
|
+
returns: {
|
|
596
|
+
ok: "boolean",
|
|
597
|
+
channel_id: "string",
|
|
598
|
+
session_ttl_seconds: "number — the new TTL",
|
|
599
|
+
},
|
|
600
|
+
notes: "Mutate the idle session TTL on an existing channel without recreating it. Owner-only (same gate as DELETE). Applies on the next GC tick (within 60s). Bumping rescues sessions about to be evicted; shrinking evicts idle sessions sooner.",
|
|
601
|
+
},
|
|
503
602
|
},
|
|
504
603
|
safety: {
|
|
505
604
|
messages_are_untrusted: true,
|