kojee-mcp 0.5.13 → 0.5.14
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/cli.js +1 -1
- package/dist/plugins/hermes/__init__.py +21 -0
- package/dist/plugins/hermes/adapter.py +481 -0
- package/dist/plugins/hermes/kojee_tandem_core.py +344 -0
- package/dist/plugins/hermes/plugin.yaml +58 -0
- package/dist/{wizard-L4MYRLJI.js → wizard-5ILBK6YD.js} +419 -30
- package/package.json +2 -2
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""Pure logic for the ``kojee-tandem`` Hermes platform adapter.
|
|
2
|
+
|
|
3
|
+
This module has ZERO Hermes imports (stdlib only) so the mapping, signature
|
|
4
|
+
verification, dedupe, and outbound-payload contracts are unit-testable on any
|
|
5
|
+
box — including one where neither Hermes nor aiohttp is installed.
|
|
6
|
+
``adapter.py`` (the Hermes-facing shim) imports from here.
|
|
7
|
+
|
|
8
|
+
Wire contracts pinned against:
|
|
9
|
+
- TandemEvent: kojee-mcp-server src/types/tandem.ts (0.5.3)
|
|
10
|
+
- Webhook signature: src/tandem/webhook-sink.ts computeWebhookSignature()
|
|
11
|
+
(hex SHA-256 HMAC of the RAW body bytes) + the emission knobs in
|
|
12
|
+
src/tandem/webhook-config.ts (default header X-Kojee-Signature with a bare
|
|
13
|
+
hex digest; "github" preset = X-Hub-Signature-256 with "sha256=" prefix).
|
|
14
|
+
- Session id derivation: src/gateway-client.ts deriveSessionId()
|
|
15
|
+
(sha256(token + "proxy") hex, first 16 chars).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import hmac
|
|
22
|
+
import json
|
|
23
|
+
from collections import OrderedDict
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Mapping, Optional
|
|
27
|
+
|
|
28
|
+
# ── Signature verification ──────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
# Default kojee-mcp emission (0.5.2 wire contract): bare hex digest.
|
|
31
|
+
KOJEE_SIGNATURE_HEADER = "X-Kojee-Signature"
|
|
32
|
+
# The "github" preset (KOJEE_WEBHOOK_SIGNATURE_FORMAT=github): sha256=<hex>.
|
|
33
|
+
GITHUB_SIGNATURE_HEADER = "X-Hub-Signature-256"
|
|
34
|
+
GITHUB_SIGNATURE_PREFIX = "sha256="
|
|
35
|
+
# Delivery id header the sink sets on every POST (event id, receiver dedupe key).
|
|
36
|
+
DELIVERY_ID_HEADER = "X-Kojee-Delivery"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def compute_signature(secret: str, raw_body: bytes) -> str:
|
|
40
|
+
"""Hex SHA-256 HMAC of the raw request-body bytes (mirror of
|
|
41
|
+
webhook-sink.ts computeWebhookSignature — same bytes, same key)."""
|
|
42
|
+
return hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _header_lookup(headers: Mapping[str, str], name: str) -> Optional[str]:
|
|
46
|
+
"""Case-insensitive header fetch from a plain mapping (aiohttp's CIMultiDict
|
|
47
|
+
is already case-insensitive, but tests use plain dicts)."""
|
|
48
|
+
getter = getattr(headers, "get", None)
|
|
49
|
+
if getter is not None:
|
|
50
|
+
direct = getter(name)
|
|
51
|
+
if direct is not None:
|
|
52
|
+
return direct
|
|
53
|
+
lowered = name.lower()
|
|
54
|
+
for key in headers:
|
|
55
|
+
if key.lower() == lowered:
|
|
56
|
+
return headers[key]
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def verify_signature(headers: Mapping[str, str], raw_body: bytes, secret: str) -> bool:
|
|
61
|
+
"""True when the request carries a valid HMAC for ``raw_body``.
|
|
62
|
+
|
|
63
|
+
Accepts EITHER emission the kojee-mcp daemon can be configured for —
|
|
64
|
+
the default ``X-Kojee-Signature: <hex>`` or the github preset
|
|
65
|
+
``X-Hub-Signature-256: sha256=<hex>`` — so a header/prefix mismatch
|
|
66
|
+
between the daemon env and the adapter config is not a bug class here.
|
|
67
|
+
Fails closed: no secret or no recognized header ⇒ False.
|
|
68
|
+
"""
|
|
69
|
+
if not secret:
|
|
70
|
+
return False
|
|
71
|
+
expected = compute_signature(secret, raw_body)
|
|
72
|
+
|
|
73
|
+
bare = _header_lookup(headers, KOJEE_SIGNATURE_HEADER)
|
|
74
|
+
if bare is not None and hmac.compare_digest(bare.strip(), expected):
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
github = _header_lookup(headers, GITHUB_SIGNATURE_HEADER)
|
|
78
|
+
if github is not None:
|
|
79
|
+
value = github.strip()
|
|
80
|
+
if value.startswith(GITHUB_SIGNATURE_PREFIX):
|
|
81
|
+
value = value[len(GITHUB_SIGNATURE_PREFIX):]
|
|
82
|
+
if hmac.compare_digest(value, expected):
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ── Inbound: TandemEvent → normalized message ───────────────────────────────
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class NormalizedTandemMessage:
|
|
92
|
+
"""A TandemEvent reduced to what a Hermes MessageEvent needs."""
|
|
93
|
+
|
|
94
|
+
chat_id: str # tandem_id — the per-tandem chat id
|
|
95
|
+
message_id: str # cursor (stringified) — stable per-tandem ordering
|
|
96
|
+
event_id: str # ULID — globally unique, the dedupe key
|
|
97
|
+
text: str # content.body
|
|
98
|
+
sender_id: str # from.principal (falls back to member_id)
|
|
99
|
+
sender_name: str # from.displayname (falls back to principal)
|
|
100
|
+
sender_session_id: Optional[str] # from.session_id (self-echo filter)
|
|
101
|
+
reply_to: Optional[str]
|
|
102
|
+
kind: str
|
|
103
|
+
severity: Optional[str]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def normalize_event(event: Mapping[str, Any]) -> Optional[NormalizedTandemMessage]:
|
|
107
|
+
"""Map a TandemEvent JSON body to a normalized message, or None when the
|
|
108
|
+
event is not an injectable chat message (state_change, empty body,
|
|
109
|
+
missing tandem_id). The adapter ACKs-and-ignores None — a non-message
|
|
110
|
+
event must never 4xx (the sink would classify it permanent and drop it,
|
|
111
|
+
or retry-burn on 5xx)."""
|
|
112
|
+
if not isinstance(event, Mapping):
|
|
113
|
+
return None
|
|
114
|
+
if event.get("type") != "message":
|
|
115
|
+
return None
|
|
116
|
+
tandem_id = event.get("tandem_id")
|
|
117
|
+
if not tandem_id or not isinstance(tandem_id, str):
|
|
118
|
+
return None
|
|
119
|
+
content = event.get("content") or {}
|
|
120
|
+
body = content.get("body") if isinstance(content, Mapping) else None
|
|
121
|
+
if not body or not isinstance(body, str):
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
sender = event.get("from") or {}
|
|
125
|
+
if not isinstance(sender, Mapping):
|
|
126
|
+
sender = {}
|
|
127
|
+
principal = sender.get("principal") or sender.get("member_id") or "unknown"
|
|
128
|
+
displayname = sender.get("displayname") or principal
|
|
129
|
+
|
|
130
|
+
cursor = event.get("cursor")
|
|
131
|
+
event_id = str(event.get("id") or "")
|
|
132
|
+
# cursor is the per-tandem message id (the directive's mapping); fall back
|
|
133
|
+
# to the ULID when a wire frame omits it so message_id is never empty.
|
|
134
|
+
message_id = str(cursor) if cursor is not None else event_id
|
|
135
|
+
|
|
136
|
+
reply_to = event.get("reply_to")
|
|
137
|
+
severity = event.get("severity")
|
|
138
|
+
|
|
139
|
+
return NormalizedTandemMessage(
|
|
140
|
+
chat_id=tandem_id,
|
|
141
|
+
message_id=message_id,
|
|
142
|
+
event_id=event_id or message_id,
|
|
143
|
+
text=body,
|
|
144
|
+
sender_id=str(principal),
|
|
145
|
+
sender_name=str(displayname),
|
|
146
|
+
sender_session_id=(
|
|
147
|
+
str(sender["session_id"]) if sender.get("session_id") else None
|
|
148
|
+
),
|
|
149
|
+
reply_to=str(reply_to) if reply_to else None,
|
|
150
|
+
kind=str(event.get("kind") or "message"),
|
|
151
|
+
severity=str(severity) if severity else None,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ── Dedupe (the receiver side of the sink's at-least-once contract) ─────────
|
|
156
|
+
|
|
157
|
+
class EventDeduper:
|
|
158
|
+
"""Bounded insertion-ordered set of seen event ids.
|
|
159
|
+
|
|
160
|
+
The webhook sink is at-least-once: after a daemon restart the
|
|
161
|
+
resubscribe-replay REDELIVERS recent events, and retries can double-send.
|
|
162
|
+
The receiver contract (recipe.ts) says: dedupe by event id / cursor.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def __init__(self, maxlen: int = 4096):
|
|
166
|
+
self._maxlen = max(1, maxlen)
|
|
167
|
+
self._seen: "OrderedDict[str, None]" = OrderedDict()
|
|
168
|
+
|
|
169
|
+
def seen_before(self, event_id: str) -> bool:
|
|
170
|
+
"""Mark ``event_id`` seen; True when it had already been seen."""
|
|
171
|
+
if event_id in self._seen:
|
|
172
|
+
self._seen.move_to_end(event_id)
|
|
173
|
+
return True
|
|
174
|
+
self._seen[event_id] = None
|
|
175
|
+
while len(self._seen) > self._maxlen:
|
|
176
|
+
self._seen.popitem(last=False)
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ── Filters ─────────────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
def parse_allowlist(raw: Optional[str]) -> frozenset:
|
|
183
|
+
"""Comma-separated tandem ids → set. Empty/None ⇒ empty set = allow all."""
|
|
184
|
+
if not raw:
|
|
185
|
+
return frozenset()
|
|
186
|
+
return frozenset(part.strip() for part in raw.split(",") if part.strip())
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def tandem_allowed(tandem_id: str, allowlist: frozenset) -> bool:
|
|
190
|
+
"""Empty allowlist ⇒ every tandem is allowed (Hermes's per-user
|
|
191
|
+
authorization still applies downstream via KOJEE_TANDEM_ALLOWED_USERS)."""
|
|
192
|
+
return not allowlist or tandem_id in allowlist
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def is_self_event(
|
|
196
|
+
msg: NormalizedTandemMessage,
|
|
197
|
+
self_session_id: Optional[str],
|
|
198
|
+
self_principal: Optional[str],
|
|
199
|
+
) -> bool:
|
|
200
|
+
"""Self-echo filter (reply-loop prevention, ADDING_A_PLATFORM checklist).
|
|
201
|
+
|
|
202
|
+
The daemon and the send helper share ONE deterministic session id
|
|
203
|
+
(sha256(token+"proxy")[:16] — gateway-client.ts deriveSessionId), so a
|
|
204
|
+
fan-out echo of our own tandem_send carries from.session_id == ours.
|
|
205
|
+
The principal check is the operator-config fallback for wire frames
|
|
206
|
+
that omit session_id.
|
|
207
|
+
"""
|
|
208
|
+
if self_session_id and msg.sender_session_id == self_session_id:
|
|
209
|
+
return True
|
|
210
|
+
if self_principal and msg.sender_id == self_principal:
|
|
211
|
+
return True
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def derive_session_id(token: str) -> str:
|
|
216
|
+
"""Mirror of GatewayClient.deriveSessionId (src/gateway-client.ts):
|
|
217
|
+
session_id = sha256(token + "proxy") hex, first 16 chars."""
|
|
218
|
+
return hashlib.sha256((token + "proxy").encode("utf-8")).hexdigest()[:16]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def load_self_session_id(kojee_dir: Path) -> Optional[str]:
|
|
222
|
+
"""Best-effort: read ~/.kojee/config.json (paired mode) and derive the
|
|
223
|
+
daemon/helper session id for the self-echo filter. None when unpaired or
|
|
224
|
+
unreadable — the adapter then relies on KOJEE_TANDEM_SELF_PRINCIPAL."""
|
|
225
|
+
try:
|
|
226
|
+
raw = (kojee_dir / "config.json").read_text(encoding="utf-8")
|
|
227
|
+
token = json.loads(raw).get("token")
|
|
228
|
+
if token and isinstance(token, str):
|
|
229
|
+
return derive_session_id(token)
|
|
230
|
+
except (OSError, ValueError):
|
|
231
|
+
pass
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ── Outbound: send → `kojee-mcp send` CLI contract ──────────────────────────
|
|
236
|
+
#
|
|
237
|
+
# T10: outbound moved from kojee_send.mjs (tsx + dynamic source imports →
|
|
238
|
+
# REQUIRED a kojee-mcp source checkout) to the shipped `kojee-mcp send`
|
|
239
|
+
# sub-command (0.5.4+). This kills the "needs a checkout" requirement — the
|
|
240
|
+
# adapter now needs only a paired ~/.kojee + the kojee-mcp binary on PATH.
|
|
241
|
+
#
|
|
242
|
+
# CLI contract (src/cli.ts + src/tandem/send.ts):
|
|
243
|
+
# kojee-mcp send <tandem_id> --body <text> [--reply-to <id>] [--kind <kind>]
|
|
244
|
+
# stdout: one JSON envelope.
|
|
245
|
+
# success: {ok:true, tandem_id, message_id, cursor, text} (exit 0)
|
|
246
|
+
# failure: {ok:false, error:<typed code>, message:<human text>} (exit 1)
|
|
247
|
+
|
|
248
|
+
KOJEE_MCP_BIN_ENV = "KOJEE_MCP_BIN"
|
|
249
|
+
# PATH fallback when KOJEE_MCP_BIN is unset (fold B).
|
|
250
|
+
DEFAULT_KOJEE_MCP_BIN = "kojee-mcp"
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def resolve_kojee_mcp_bin(env: Mapping[str, str]) -> str:
|
|
254
|
+
"""The kojee-mcp binary the outbound send shells.
|
|
255
|
+
|
|
256
|
+
Fold B: the plugin runs inside the gateway process, where the login-shell
|
|
257
|
+
PATH may not apply — so the installer writes an ABSOLUTE path into
|
|
258
|
+
``KOJEE_MCP_BIN``. When that env is unset/empty we fall back to the bare
|
|
259
|
+
``kojee-mcp`` name and rely on PATH resolution.
|
|
260
|
+
"""
|
|
261
|
+
# TODO(hermes-installer): wizard writes KOJEE_MCP_BIN at install
|
|
262
|
+
# (src/wizard/installers/hermes.ts) — until then this fallback carries it.
|
|
263
|
+
return env.get(KOJEE_MCP_BIN_ENV) or DEFAULT_KOJEE_MCP_BIN
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def build_send_command(
|
|
267
|
+
bin_path: str,
|
|
268
|
+
tandem_id: str,
|
|
269
|
+
body: str,
|
|
270
|
+
reply_to: Optional[str] = None,
|
|
271
|
+
kind: Optional[str] = None,
|
|
272
|
+
) -> list:
|
|
273
|
+
"""Argv for `kojee-mcp send <tandem_id> --body <text> [--reply-to] [--kind]`.
|
|
274
|
+
|
|
275
|
+
``bin_path`` is the caller-resolved kojee-mcp binary (see
|
|
276
|
+
resolve_kojee_mcp_bin). body/reply_to/kind ride argv directly (asyncio's
|
|
277
|
+
exec passes argv to the kernel — no shell, so no quoting hazard).
|
|
278
|
+
reply_to/kind are OMITTED when absent, never passed empty.
|
|
279
|
+
"""
|
|
280
|
+
argv = [bin_path, "send", tandem_id, "--body", body]
|
|
281
|
+
if reply_to:
|
|
282
|
+
argv += ["--reply-to", reply_to]
|
|
283
|
+
if kind:
|
|
284
|
+
argv += ["--kind", kind]
|
|
285
|
+
return argv
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def parse_send_envelope(stdout: str) -> dict:
|
|
289
|
+
"""Parse the `kojee-mcp send` JSON envelope from stdout.
|
|
290
|
+
|
|
291
|
+
Returns a normalized dict::
|
|
292
|
+
|
|
293
|
+
{"ok": bool,
|
|
294
|
+
"message_id": Optional[str],
|
|
295
|
+
"cursor": Optional[int],
|
|
296
|
+
"error": Optional[str], # human-readable failure text
|
|
297
|
+
"error_code": Optional[str]} # the typed SendErrorCode
|
|
298
|
+
|
|
299
|
+
Tolerates noise lines before the envelope (node warnings) by scanning for
|
|
300
|
+
the LAST parseable JSON object carrying ``ok``. No envelope ⇒ ok=False with
|
|
301
|
+
the raw tail as the error — a CLI crash must surface, not masquerade as a
|
|
302
|
+
delivered send.
|
|
303
|
+
"""
|
|
304
|
+
envelope = None
|
|
305
|
+
for line in stdout.splitlines():
|
|
306
|
+
line = line.strip()
|
|
307
|
+
if not line.startswith("{"):
|
|
308
|
+
continue
|
|
309
|
+
try:
|
|
310
|
+
candidate = json.loads(line)
|
|
311
|
+
except ValueError:
|
|
312
|
+
continue
|
|
313
|
+
if isinstance(candidate, dict) and "ok" in candidate:
|
|
314
|
+
envelope = candidate
|
|
315
|
+
if envelope is None:
|
|
316
|
+
tail = stdout.strip()[-300:] if stdout.strip() else "(no output)"
|
|
317
|
+
return {
|
|
318
|
+
"ok": False,
|
|
319
|
+
"message_id": None,
|
|
320
|
+
"cursor": None,
|
|
321
|
+
"error": f"kojee-mcp send produced no envelope: {tail}",
|
|
322
|
+
"error_code": None,
|
|
323
|
+
}
|
|
324
|
+
if envelope.get("ok"):
|
|
325
|
+
raw_cursor = envelope.get("cursor")
|
|
326
|
+
return {
|
|
327
|
+
"ok": True,
|
|
328
|
+
"message_id": (
|
|
329
|
+
str(envelope["message_id"]) if envelope.get("message_id") else None
|
|
330
|
+
),
|
|
331
|
+
"cursor": raw_cursor if isinstance(raw_cursor, int) else None,
|
|
332
|
+
"error": None,
|
|
333
|
+
"error_code": None,
|
|
334
|
+
}
|
|
335
|
+
# Failure envelope: {ok:false, error:<typed code>, message:<human text>}.
|
|
336
|
+
code = str(envelope["error"]) if envelope.get("error") else None
|
|
337
|
+
message = str(envelope["message"]) if envelope.get("message") else None
|
|
338
|
+
return {
|
|
339
|
+
"ok": False,
|
|
340
|
+
"message_id": None,
|
|
341
|
+
"cursor": None,
|
|
342
|
+
"error": message or code or "kojee-mcp send failed",
|
|
343
|
+
"error_code": code,
|
|
344
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
name: kojee-tandem
|
|
2
|
+
label: Kojee Tandem
|
|
3
|
+
kind: platform
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
description: >
|
|
6
|
+
Kojee Tandem gateway adapter for Hermes Agent (v1, sidecar architecture).
|
|
7
|
+
Makes Tandem — Kojee's shared human+agent conversations — a first-class
|
|
8
|
+
Hermes channel: the kojee-mcp daemon (DPoP auth, SSE event stream) delivers
|
|
9
|
+
Tandem messages to this adapter's loopback webhook listener, and replies go
|
|
10
|
+
out through the shipped `kojee-mcp send` CLI via tandem_send. Needs a paired
|
|
11
|
+
~/.kojee and the kojee-mcp binary on the box (no source checkout required).
|
|
12
|
+
author: Kojee
|
|
13
|
+
requires_env:
|
|
14
|
+
- name: KOJEE_WEBHOOK_SECRET
|
|
15
|
+
description: "HMAC secret shared with the kojee-mcp daemon's webhook sink (the adapter rejects unsigned events)"
|
|
16
|
+
prompt: "Webhook HMAC secret"
|
|
17
|
+
password: true
|
|
18
|
+
optional_env:
|
|
19
|
+
- name: KOJEE_MCP_BIN
|
|
20
|
+
description: "Absolute path to the kojee-mcp binary the outbound `send` shells (default: `kojee-mcp` on PATH). Set when the binary is not on the gateway process PATH."
|
|
21
|
+
prompt: "kojee-mcp binary path (optional)"
|
|
22
|
+
password: false
|
|
23
|
+
- name: KOJEE_TANDEM_LISTEN_HOST
|
|
24
|
+
description: "Webhook listener bind host (default 127.0.0.1 — keep it loopback)"
|
|
25
|
+
prompt: "Listen host"
|
|
26
|
+
password: false
|
|
27
|
+
- name: KOJEE_TANDEM_LISTEN_PORT
|
|
28
|
+
description: "Webhook listener port (default 8645); the daemon's KOJEE_WEBHOOK_URL must point here"
|
|
29
|
+
prompt: "Listen port"
|
|
30
|
+
password: false
|
|
31
|
+
- name: KOJEE_TANDEM_LISTEN_PATH
|
|
32
|
+
description: "Webhook listener path (default /kojee-tandem)"
|
|
33
|
+
prompt: "Listen path"
|
|
34
|
+
password: false
|
|
35
|
+
- name: KOJEE_TANDEM_ALLOWLIST
|
|
36
|
+
description: "Comma-separated tandem ids allowed to wake the agent (empty = all tandems)"
|
|
37
|
+
prompt: "Tandem allowlist (comma-separated, or empty)"
|
|
38
|
+
password: false
|
|
39
|
+
- name: KOJEE_TANDEM_ALLOWED_USERS
|
|
40
|
+
description: "Comma-separated Tandem principals allowed to talk to the bot (e.g. user:daria@cohen.io)"
|
|
41
|
+
prompt: "Allowed principals (comma-separated)"
|
|
42
|
+
password: false
|
|
43
|
+
- name: KOJEE_TANDEM_ALLOW_ALL_USERS
|
|
44
|
+
description: "Allow every tandem member to talk to the bot (1/true/yes)"
|
|
45
|
+
prompt: "Allow all users? (true/false)"
|
|
46
|
+
password: false
|
|
47
|
+
- name: KOJEE_TANDEM_SELF_PRINCIPAL
|
|
48
|
+
description: "Extra self-echo filter for wire frames without sender session_id. WARNING: setting this to the shared account principal silences EVERYONE sharing it, including your owner (the principal is shared by the human owner and all sibling agents) — defaults are safe, leave unset"
|
|
49
|
+
prompt: "Self principal (optional)"
|
|
50
|
+
password: false
|
|
51
|
+
- name: KOJEE_TANDEM_HOME_CHANNEL
|
|
52
|
+
description: "Default tandem id for cron / notification delivery (deliver=kojee-tandem)"
|
|
53
|
+
prompt: "Home tandem id (optional)"
|
|
54
|
+
password: false
|
|
55
|
+
- name: KOJEE_DIR
|
|
56
|
+
description: "Kojee state dir holding config.json + keypair.json (default ~/.kojee)"
|
|
57
|
+
prompt: "Kojee dir (optional)"
|
|
58
|
+
password: false
|