akemon 0.3.5 → 0.3.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/DATA_POLICY.md +128 -0
- package/README.md +156 -19
- package/TRADEMARK.md +74 -0
- package/dist/akemon-home.js +56 -0
- package/dist/akemon-message.js +107 -0
- package/dist/best-effort.js +8 -0
- package/dist/cli.js +1411 -132
- package/dist/cognitive-artifact-store.js +101 -0
- package/dist/cognitive-event-log.js +47 -0
- package/dist/config.js +45 -9
- package/dist/context.js +27 -6
- package/dist/core/contracts/layers.js +1 -0
- package/dist/core/contracts/permission.js +1 -0
- package/dist/core/contracts/workspace.js +1 -0
- package/dist/core-cognitive-module.js +768 -0
- package/dist/engine-peripheral.js +127 -26
- package/dist/engine-routing.js +58 -17
- package/dist/interactive-session.js +361 -0
- package/dist/local-interconnect.js +156 -0
- package/dist/local-registry.js +178 -0
- package/dist/mcp-server.js +4 -1
- package/dist/memory-proposal.js +379 -0
- package/dist/memory-recorder.js +368 -0
- package/dist/orphan-scan.js +36 -24
- package/dist/passive-reflection-cognitive-module.js +172 -0
- package/dist/peripheral-registry.js +235 -0
- package/dist/permission-audit.js +132 -0
- package/dist/relay-client.js +68 -9
- package/dist/relay-mode.js +34 -0
- package/dist/relay-peripheral.js +139 -49
- package/dist/runtime-platform.js +122 -0
- package/dist/secretariat/client.js +87 -0
- package/dist/self.js +15 -6
- package/dist/server.js +3695 -439
- package/dist/social-discovery.js +231 -0
- package/dist/software-agent-peripheral.js +314 -235
- package/dist/software-agent-result-cli.js +69 -0
- package/dist/software-agent-stream-cli.js +23 -0
- package/dist/software-agent-transport.js +177 -0
- package/dist/task-module.js +243 -0
- package/dist/task-registry.js +756 -0
- package/dist/vendor/xterm/addon-fit.js +2 -0
- package/dist/vendor/xterm/addon-search.js +2 -0
- package/dist/vendor/xterm/addon-web-links.js +2 -0
- package/dist/vendor/xterm/xterm.css +285 -0
- package/dist/vendor/xterm/xterm.js +2 -0
- package/dist/work-memory.js +339 -0
- package/dist/workbench-peripheral-guide.js +79 -0
- package/dist/workbench-session.js +1074 -0
- package/dist/workbench.html +4011 -0
- package/package.json +11 -4
- package/scripts/build.cjs +24 -0
- package/scripts/check-architecture-baseline.cjs +68 -0
- package/scripts/test.cjs +38 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
2
|
+
import { dirname } from "path";
|
|
3
|
+
import { agentPeripheralRegistryPath } from "./akemon-home.js";
|
|
4
|
+
import { redactText } from "./redaction.js";
|
|
5
|
+
const VALID_TYPES = ["engine", "relay", "software-agent", "interactive-session", "mcp", "service", "hardware", "custom"];
|
|
6
|
+
const VALID_RISK_LEVELS = ["low", "medium", "high"];
|
|
7
|
+
const VALID_EXPLORE_MODES = ["none", "plain-text"];
|
|
8
|
+
const VALID_SOURCES = ["runtime", "owner", "imported"];
|
|
9
|
+
const VALID_STATUSES = ["configured", "available", "unavailable"];
|
|
10
|
+
export function peripheralRegistryPath(agentName) {
|
|
11
|
+
return agentPeripheralRegistryPath(agentName);
|
|
12
|
+
}
|
|
13
|
+
export async function loadPeripheralRecords(agentName) {
|
|
14
|
+
let raw;
|
|
15
|
+
try {
|
|
16
|
+
raw = await readFile(peripheralRegistryPath(agentName), "utf-8");
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.code === "ENOENT")
|
|
20
|
+
return [];
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
const records = Array.isArray(parsed.records) ? parsed.records : [];
|
|
25
|
+
return records.map((record) => normalizePeripheralRecord(record));
|
|
26
|
+
}
|
|
27
|
+
export async function savePeripheralRecords(agentName, records) {
|
|
28
|
+
const path = peripheralRegistryPath(agentName);
|
|
29
|
+
const normalized = records.map((record) => normalizePeripheralRecord(record));
|
|
30
|
+
const file = {
|
|
31
|
+
schemaVersion: 1,
|
|
32
|
+
records: normalized.sort((a, b) => a.id.localeCompare(b.id)),
|
|
33
|
+
};
|
|
34
|
+
await mkdir(dirname(path), { recursive: true });
|
|
35
|
+
await writeFile(path, `${JSON.stringify(file, null, 2)}\n`, "utf-8");
|
|
36
|
+
}
|
|
37
|
+
export async function upsertPeripheralRecord(agentName, record) {
|
|
38
|
+
const normalized = normalizePeripheralRecord(record);
|
|
39
|
+
const records = await loadPeripheralRecords(agentName);
|
|
40
|
+
const next = upsertRecords(records, [normalized]);
|
|
41
|
+
await savePeripheralRecords(agentName, next);
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
44
|
+
export async function upsertPeripheralRecords(agentName, records) {
|
|
45
|
+
const normalized = records.map((record) => normalizePeripheralRecord(record));
|
|
46
|
+
const existing = await loadPeripheralRecords(agentName);
|
|
47
|
+
const next = upsertRecords(existing, normalized);
|
|
48
|
+
await savePeripheralRecords(agentName, next);
|
|
49
|
+
return normalized;
|
|
50
|
+
}
|
|
51
|
+
export function createRuntimePeripheralRecord(peripheral, options = {}) {
|
|
52
|
+
return normalizePeripheralRecord({
|
|
53
|
+
schemaVersion: 1,
|
|
54
|
+
id: peripheral.id,
|
|
55
|
+
name: peripheral.name,
|
|
56
|
+
type: options.type || inferPeripheralType(peripheral),
|
|
57
|
+
capabilities: peripheral.capabilities,
|
|
58
|
+
tags: peripheral.tags,
|
|
59
|
+
riskLevel: options.riskLevel || inferRiskLevel(peripheral),
|
|
60
|
+
allowedActions: options.allowedActions || [],
|
|
61
|
+
explore: {
|
|
62
|
+
mode: peripheral.explore ? "plain-text" : "none",
|
|
63
|
+
description: options.exploreDescription,
|
|
64
|
+
},
|
|
65
|
+
source: "runtime",
|
|
66
|
+
status: options.status || "available",
|
|
67
|
+
updatedAt: options.updatedAt || new Date().toISOString(),
|
|
68
|
+
startCommand: options.startCommand,
|
|
69
|
+
url: options.url,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export function mergePeripheralRecords(configured, runtime) {
|
|
73
|
+
const byId = new Map();
|
|
74
|
+
for (const record of configured)
|
|
75
|
+
byId.set(record.id, normalizePeripheralRecord(record));
|
|
76
|
+
for (const record of runtime)
|
|
77
|
+
byId.set(record.id, normalizePeripheralRecord(record));
|
|
78
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
79
|
+
}
|
|
80
|
+
export async function buildPeripheralExploreBriefing(input) {
|
|
81
|
+
const generatedAt = new Date().toISOString();
|
|
82
|
+
const runtimeById = new Map((input.runtimePeripherals || []).map((peripheral) => [peripheral.id, peripheral]));
|
|
83
|
+
const selected = input.id
|
|
84
|
+
? input.records.filter((record) => record.id === input.id)
|
|
85
|
+
: [...input.records];
|
|
86
|
+
const sections = [];
|
|
87
|
+
for (const record of selected) {
|
|
88
|
+
const lines = renderRegistryBriefingLines(record);
|
|
89
|
+
const runtime = runtimeById.get(record.id);
|
|
90
|
+
if (runtime?.explore) {
|
|
91
|
+
try {
|
|
92
|
+
const live = (await runtime.explore()).trim();
|
|
93
|
+
if (live) {
|
|
94
|
+
lines.push("live briefing:");
|
|
95
|
+
for (const line of redactText(live).split(/\r?\n/)) {
|
|
96
|
+
lines.push(` ${line}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
102
|
+
lines.push(`live briefing error: ${redactText(message)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
sections.push({
|
|
106
|
+
id: record.id,
|
|
107
|
+
name: record.name,
|
|
108
|
+
type: record.type,
|
|
109
|
+
text: lines.join("\n"),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const text = sections.map((section) => section.text).join("\n\n") || "(no peripherals registered)";
|
|
113
|
+
return { generatedAt, sections, text };
|
|
114
|
+
}
|
|
115
|
+
export function normalizePeripheralRecord(input) {
|
|
116
|
+
const now = new Date().toISOString();
|
|
117
|
+
const type = cleanEnum(input.type, VALID_TYPES, "type", "custom");
|
|
118
|
+
const riskLevel = cleanEnum(input.riskLevel, VALID_RISK_LEVELS, "riskLevel", "medium");
|
|
119
|
+
const exploreMode = cleanEnum(input.explore?.mode, VALID_EXPLORE_MODES, "explore.mode", "none");
|
|
120
|
+
return {
|
|
121
|
+
schemaVersion: 1,
|
|
122
|
+
id: cleanPeripheralId(input.id),
|
|
123
|
+
name: cleanText(input.name || input.id, "name", 160),
|
|
124
|
+
type,
|
|
125
|
+
capabilities: cleanTokenList(input.capabilities || [], "capabilities"),
|
|
126
|
+
tags: cleanTokenList(input.tags || [], "tags"),
|
|
127
|
+
riskLevel,
|
|
128
|
+
allowedActions: cleanTokenList(input.allowedActions || [], "allowedActions"),
|
|
129
|
+
explore: {
|
|
130
|
+
mode: exploreMode,
|
|
131
|
+
...(input.explore?.description ? { description: cleanText(input.explore.description, "explore.description", 500) } : {}),
|
|
132
|
+
},
|
|
133
|
+
source: cleanEnum(input.source, VALID_SOURCES, "source", "owner"),
|
|
134
|
+
status: cleanEnum(input.status, VALID_STATUSES, "status", "configured"),
|
|
135
|
+
updatedAt: cleanText(input.updatedAt || now, "updatedAt", 80),
|
|
136
|
+
...(input.startCommand ? { startCommand: cleanCommand(input.startCommand) } : {}),
|
|
137
|
+
...(input.url ? { url: cleanUrl(input.url) } : {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function upsertRecords(existing, records) {
|
|
141
|
+
const byId = new Map();
|
|
142
|
+
for (const record of existing)
|
|
143
|
+
byId.set(record.id, normalizePeripheralRecord(record));
|
|
144
|
+
for (const record of records)
|
|
145
|
+
byId.set(record.id, normalizePeripheralRecord(record));
|
|
146
|
+
return [...byId.values()];
|
|
147
|
+
}
|
|
148
|
+
function renderRegistryBriefingLines(record) {
|
|
149
|
+
const lines = [
|
|
150
|
+
`[${record.id}] ${record.name}`,
|
|
151
|
+
`type=${record.type} risk=${record.riskLevel} status=${record.status} source=${record.source}`,
|
|
152
|
+
`capabilities: ${record.capabilities.length ? record.capabilities.join(", ") : "(none declared)"}`,
|
|
153
|
+
];
|
|
154
|
+
if (record.tags.length)
|
|
155
|
+
lines.push(`tags: ${record.tags.join(", ")}`);
|
|
156
|
+
if (record.allowedActions.length)
|
|
157
|
+
lines.push(`allowed actions: ${record.allowedActions.join(", ")}`);
|
|
158
|
+
if (record.startCommand)
|
|
159
|
+
lines.push(`start command: ${redactText(record.startCommand)}`);
|
|
160
|
+
if (record.url)
|
|
161
|
+
lines.push(`url: ${redactText(record.url)}`);
|
|
162
|
+
lines.push(`explore: ${record.explore.mode}${record.explore.description ? ` - ${record.explore.description}` : ""}`);
|
|
163
|
+
return lines;
|
|
164
|
+
}
|
|
165
|
+
function inferPeripheralType(peripheral) {
|
|
166
|
+
if (peripheral.tags.includes("engine") || peripheral.id.startsWith("engine:"))
|
|
167
|
+
return "engine";
|
|
168
|
+
if (peripheral.tags.includes("relay") || peripheral.id === "relay")
|
|
169
|
+
return "relay";
|
|
170
|
+
if (peripheral.tags.includes("software-agent") || peripheral.id.startsWith("software-agent:"))
|
|
171
|
+
return "software-agent";
|
|
172
|
+
return "custom";
|
|
173
|
+
}
|
|
174
|
+
function inferRiskLevel(peripheral) {
|
|
175
|
+
if (peripheral.capabilities.includes("repo-edit") || peripheral.capabilities.includes("action-out"))
|
|
176
|
+
return "high";
|
|
177
|
+
if (peripheral.capabilities.includes("sync") || peripheral.capabilities.includes("tool-use"))
|
|
178
|
+
return "medium";
|
|
179
|
+
return "low";
|
|
180
|
+
}
|
|
181
|
+
function cleanPeripheralId(value) {
|
|
182
|
+
const cleaned = cleanText(value, "id", 120);
|
|
183
|
+
if (/[/\\\x00-\x1f\x7f]/.test(cleaned))
|
|
184
|
+
throw new Error("Invalid peripheral id: must not contain path separators or control characters");
|
|
185
|
+
if (!/^[A-Za-z0-9_.:-]+$/.test(cleaned)) {
|
|
186
|
+
throw new Error("Invalid peripheral id: use only letters, numbers, '.', '_', ':', and '-'");
|
|
187
|
+
}
|
|
188
|
+
return cleaned;
|
|
189
|
+
}
|
|
190
|
+
function cleanTokenList(values, label) {
|
|
191
|
+
const seen = new Set();
|
|
192
|
+
const result = [];
|
|
193
|
+
for (const value of values) {
|
|
194
|
+
const cleaned = cleanText(value, label, 100);
|
|
195
|
+
if (/[/\\\x00-\x1f\x7f]/.test(cleaned))
|
|
196
|
+
throw new Error(`Invalid ${label}: contains path separators or control characters`);
|
|
197
|
+
if (!seen.has(cleaned)) {
|
|
198
|
+
seen.add(cleaned);
|
|
199
|
+
result.push(cleaned);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
function cleanText(value, label, max) {
|
|
205
|
+
const cleaned = String(value || "").trim();
|
|
206
|
+
if (!cleaned)
|
|
207
|
+
throw new Error(`Invalid peripheral ${label}: must not be empty`);
|
|
208
|
+
if (cleaned.length > max)
|
|
209
|
+
throw new Error(`Invalid peripheral ${label}: too long`);
|
|
210
|
+
if (/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(cleaned))
|
|
211
|
+
throw new Error(`Invalid peripheral ${label}: contains control characters`);
|
|
212
|
+
return cleaned;
|
|
213
|
+
}
|
|
214
|
+
function cleanCommand(value) {
|
|
215
|
+
const cleaned = cleanText(value, "startCommand", 1000);
|
|
216
|
+
if (/[\r\n]/.test(cleaned))
|
|
217
|
+
throw new Error("Invalid peripheral startCommand: must be a single line");
|
|
218
|
+
return cleaned;
|
|
219
|
+
}
|
|
220
|
+
function cleanUrl(value) {
|
|
221
|
+
const cleaned = cleanText(value, "url", 1000);
|
|
222
|
+
const url = new URL(cleaned);
|
|
223
|
+
if (!["http:", "https:", "ws:", "wss:", "file:"].includes(url.protocol)) {
|
|
224
|
+
throw new Error("Invalid peripheral url: unsupported protocol");
|
|
225
|
+
}
|
|
226
|
+
return url.toString();
|
|
227
|
+
}
|
|
228
|
+
function cleanEnum(value, allowed, label, fallback) {
|
|
229
|
+
if (value === undefined || value === null || value === "")
|
|
230
|
+
return fallback;
|
|
231
|
+
const normalized = String(value).trim().toLowerCase();
|
|
232
|
+
if (allowed.includes(normalized))
|
|
233
|
+
return normalized;
|
|
234
|
+
throw new Error(`Invalid peripheral ${label}: must be one of ${allowed.join(", ")}`);
|
|
235
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { appendFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { appendFile, mkdir, readFile } from "fs/promises";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { agentAuditDir, cleanAgentName } from "./akemon-home.js";
|
|
6
|
+
import { redactSecrets } from "./redaction.js";
|
|
7
|
+
const DEFAULT_AUDIT_LIMIT = 50;
|
|
8
|
+
const MAX_AUDIT_LIMIT = 500;
|
|
9
|
+
export function permissionAuditLogPath(agentName) {
|
|
10
|
+
return join(agentAuditDir(agentName), "actions.jsonl");
|
|
11
|
+
}
|
|
12
|
+
export function createPermissionAuditRecord(agentName, input) {
|
|
13
|
+
return {
|
|
14
|
+
schemaVersion: 1,
|
|
15
|
+
id: input.id || randomUUID(),
|
|
16
|
+
ts: input.ts || new Date().toISOString(),
|
|
17
|
+
agentName: cleanAgentName(agentName),
|
|
18
|
+
actionKind: input.actionKind,
|
|
19
|
+
action: cleanAuditToken(input.action, "action"),
|
|
20
|
+
requestedBy: normalizeActor(input.requestedBy),
|
|
21
|
+
performedBy: normalizeActor(input.performedBy),
|
|
22
|
+
...(input.target ? { target: normalizeActor(input.target) } : {}),
|
|
23
|
+
...(input.sourceModule ? { sourceModule: cleanAuditToken(input.sourceModule, "sourceModule") } : {}),
|
|
24
|
+
...(input.peripheralId ? { peripheralId: cleanAuditToken(input.peripheralId, "peripheralId") } : {}),
|
|
25
|
+
...(input.riskLevel ? { riskLevel: input.riskLevel } : {}),
|
|
26
|
+
...(input.roleScope ? { roleScope: cleanAuditToken(input.roleScope, "roleScope") } : {}),
|
|
27
|
+
...(input.memoryScope ? { memoryScope: input.memoryScope } : {}),
|
|
28
|
+
...(input.workdir ? { workdir: input.workdir } : {}),
|
|
29
|
+
...(input.projectScope ? { projectScope: input.projectScope } : {}),
|
|
30
|
+
...(input.transport ? { transport: input.transport } : {}),
|
|
31
|
+
decision: {
|
|
32
|
+
result: input.decision.result,
|
|
33
|
+
mode: input.decision.mode,
|
|
34
|
+
...(input.decision.reason ? { reason: truncate(input.decision.reason, 500) } : {}),
|
|
35
|
+
},
|
|
36
|
+
...(input.allowedActions?.length ? { allowedActions: input.allowedActions.map((item) => truncate(item, 300)) } : {}),
|
|
37
|
+
...(input.forbiddenActions?.length ? { forbiddenActions: input.forbiddenActions.map((item) => truncate(item, 300)) } : {}),
|
|
38
|
+
...(input.references ? { references: compactStringRecord(input.references) } : {}),
|
|
39
|
+
...(input.metadata ? { metadata: redactSecrets(input.metadata) } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export async function appendPermissionAuditRecord(agentName, input) {
|
|
43
|
+
const record = isPermissionAuditRecord(input) ? input : createPermissionAuditRecord(agentName, input);
|
|
44
|
+
const file = permissionAuditLogPath(record.agentName);
|
|
45
|
+
await mkdir(agentAuditDir(record.agentName), { recursive: true });
|
|
46
|
+
await appendFile(file, `${renderAuditRecord(record)}\n`, "utf-8");
|
|
47
|
+
return file;
|
|
48
|
+
}
|
|
49
|
+
export function appendPermissionAuditRecordSync(agentName, input) {
|
|
50
|
+
const record = isPermissionAuditRecord(input) ? input : createPermissionAuditRecord(agentName, input);
|
|
51
|
+
const file = permissionAuditLogPath(record.agentName);
|
|
52
|
+
mkdirSync(agentAuditDir(record.agentName), { recursive: true });
|
|
53
|
+
appendFileSync(file, `${renderAuditRecord(record)}\n`, "utf-8");
|
|
54
|
+
return file;
|
|
55
|
+
}
|
|
56
|
+
export async function listPermissionAuditRecords(agentName, options = {}) {
|
|
57
|
+
let raw = "";
|
|
58
|
+
try {
|
|
59
|
+
raw = await readFile(permissionAuditLogPath(agentName), "utf-8");
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
const limit = clampLimit(options.limit);
|
|
65
|
+
const records = [];
|
|
66
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
67
|
+
if (!line.trim())
|
|
68
|
+
continue;
|
|
69
|
+
try {
|
|
70
|
+
const record = JSON.parse(line);
|
|
71
|
+
if (!isPermissionAuditRecord(record))
|
|
72
|
+
continue;
|
|
73
|
+
if (options.actionKind && record.actionKind !== options.actionKind)
|
|
74
|
+
continue;
|
|
75
|
+
if (options.decision && record.decision.result !== options.decision)
|
|
76
|
+
continue;
|
|
77
|
+
records.push(record);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Ignore malformed audit lines; the log is append-only and best-effort.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return records.sort((a, b) => b.ts.localeCompare(a.ts)).slice(0, limit);
|
|
84
|
+
}
|
|
85
|
+
function renderAuditRecord(record) {
|
|
86
|
+
return redactSecrets(JSON.stringify(record));
|
|
87
|
+
}
|
|
88
|
+
function isPermissionAuditRecord(value) {
|
|
89
|
+
if (!value || typeof value !== "object")
|
|
90
|
+
return false;
|
|
91
|
+
const record = value;
|
|
92
|
+
return record.schemaVersion === 1
|
|
93
|
+
&& typeof record.id === "string"
|
|
94
|
+
&& typeof record.ts === "string"
|
|
95
|
+
&& typeof record.agentName === "string"
|
|
96
|
+
&& typeof record.actionKind === "string"
|
|
97
|
+
&& typeof record.action === "string"
|
|
98
|
+
&& !!record.requestedBy
|
|
99
|
+
&& !!record.performedBy
|
|
100
|
+
&& !!record.decision;
|
|
101
|
+
}
|
|
102
|
+
function normalizeActor(actor) {
|
|
103
|
+
return {
|
|
104
|
+
kind: actor.kind,
|
|
105
|
+
id: truncate(String(actor.id || "unknown").trim() || "unknown", 160),
|
|
106
|
+
...(actor.transport ? { transport: actor.transport } : {}),
|
|
107
|
+
...(actor.displayName ? { displayName: truncate(actor.displayName, 160) } : {}),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function compactStringRecord(record) {
|
|
111
|
+
const out = {};
|
|
112
|
+
for (const [key, value] of Object.entries(record)) {
|
|
113
|
+
if (typeof value !== "string" || !value)
|
|
114
|
+
continue;
|
|
115
|
+
out[cleanAuditToken(key, "referenceKey")] = truncate(value, 500);
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
function cleanAuditToken(value, field) {
|
|
120
|
+
const cleaned = value.trim().replace(/\s+/g, "-").slice(0, 120);
|
|
121
|
+
if (!cleaned)
|
|
122
|
+
throw new Error(`Invalid ${field}: empty`);
|
|
123
|
+
return cleaned;
|
|
124
|
+
}
|
|
125
|
+
function truncate(value, max) {
|
|
126
|
+
return value.length <= max ? value : value.slice(0, max);
|
|
127
|
+
}
|
|
128
|
+
function clampLimit(value) {
|
|
129
|
+
if (!Number.isInteger(value) || !value || value <= 0)
|
|
130
|
+
return DEFAULT_AUDIT_LIMIT;
|
|
131
|
+
return Math.min(value, MAX_AUDIT_LIMIT);
|
|
132
|
+
}
|
package/dist/relay-client.js
CHANGED
|
@@ -2,6 +2,8 @@ import WebSocket from "ws";
|
|
|
2
2
|
import http from "http";
|
|
3
3
|
import { getMetrics, updateMetrics } from "./metrics.js";
|
|
4
4
|
import { redactText, StreamingRedactor } from "./redaction.js";
|
|
5
|
+
import { getRuntimePlatform } from "./runtime-platform.js";
|
|
6
|
+
import { createRelayAgentCallMessage, createRelayAgentCallResultMessage, isAkemonMessageEnvelope, textFromAkemonMessage, } from "./akemon-message.js";
|
|
5
7
|
const DEFAULT_RELAY_URL = "wss://relay.akemon.dev";
|
|
6
8
|
// Pending agent_call results (callId → resolve function)
|
|
7
9
|
const pendingAgentCalls = new Map();
|
|
@@ -26,9 +28,16 @@ function startPTY(ws, cols, rows) {
|
|
|
26
28
|
console.log("[terminal] PTY already running, ignoring duplicate start");
|
|
27
29
|
return;
|
|
28
30
|
}
|
|
31
|
+
const runtime = getRuntimePlatform();
|
|
32
|
+
if (!runtime.capabilities.canStartPty) {
|
|
33
|
+
const error = `Terminal PTY is not supported on this platform: ${runtime.platform}`;
|
|
34
|
+
console.error(`[terminal] ${error}`);
|
|
35
|
+
ws.send(JSON.stringify({ type: "terminal_exit", error }));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
29
38
|
// Dynamic import so node-pty is only loaded when needed
|
|
30
39
|
import("node-pty").then((pty) => {
|
|
31
|
-
const shell =
|
|
40
|
+
const shell = runtime.defaultShell;
|
|
32
41
|
ptyProcess = pty.spawn(shell, [], {
|
|
33
42
|
name: "xterm-256color",
|
|
34
43
|
cols: cols || 80,
|
|
@@ -65,19 +74,33 @@ function stopPTY() {
|
|
|
65
74
|
}
|
|
66
75
|
}
|
|
67
76
|
/** Call another agent through the relay. Available to any engine. */
|
|
68
|
-
export function callAgent(target, task) {
|
|
77
|
+
export function callAgent(target, task, options = {}) {
|
|
69
78
|
return new Promise((resolve, reject) => {
|
|
70
79
|
if (!relayWsRef || relayWsRef.readyState !== WebSocket.OPEN) {
|
|
71
80
|
reject(new Error("Not connected to relay"));
|
|
72
81
|
return;
|
|
73
82
|
}
|
|
74
83
|
const callId = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
84
|
+
const akemonMessage = createRelayAgentCallMessage({
|
|
85
|
+
callId,
|
|
86
|
+
caller: options.sourceAgent || "unknown",
|
|
87
|
+
target,
|
|
88
|
+
task,
|
|
89
|
+
conversationId: options.conversationId,
|
|
90
|
+
memoryScope: options.memoryScope,
|
|
91
|
+
requiresOwnerApproval: options.requiresOwnerApproval,
|
|
92
|
+
});
|
|
75
93
|
pendingAgentCalls.set(callId, resolve);
|
|
76
94
|
relayWsRef.send(JSON.stringify({
|
|
77
95
|
type: "agent_call",
|
|
78
96
|
call_id: callId,
|
|
97
|
+
...(options.sourceAgent ? { caller: options.sourceAgent } : {}),
|
|
79
98
|
target,
|
|
80
99
|
task,
|
|
100
|
+
conversation_id: options.conversationId,
|
|
101
|
+
memory_scope: akemonMessage.memoryScope,
|
|
102
|
+
require_owner_approval: akemonMessage.permissions.requiresOwnerApproval === true,
|
|
103
|
+
akemon_message: akemonMessage,
|
|
81
104
|
}));
|
|
82
105
|
// Timeout after 5 minutes
|
|
83
106
|
setTimeout(() => {
|
|
@@ -254,7 +277,7 @@ export function connectRelay(options) {
|
|
|
254
277
|
handleControl(ws, msg);
|
|
255
278
|
break;
|
|
256
279
|
case "agent_call":
|
|
257
|
-
handleIncomingAgentCall(ws, msg, options.localPort);
|
|
280
|
+
handleIncomingAgentCall(ws, msg, options.localPort, options.agentName);
|
|
258
281
|
break;
|
|
259
282
|
case "agent_call_result":
|
|
260
283
|
handleAgentCallResult(msg);
|
|
@@ -310,11 +333,22 @@ export function connectRelay(options) {
|
|
|
310
333
|
}
|
|
311
334
|
connect();
|
|
312
335
|
}
|
|
313
|
-
function handleIncomingAgentCall(ws, msg, localPort) {
|
|
336
|
+
function handleIncomingAgentCall(ws, msg, localPort, agentName) {
|
|
314
337
|
const callId = msg.call_id || "";
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
338
|
+
const incomingMessage = isAkemonMessageEnvelope(msg.akemon_message)
|
|
339
|
+
? msg.akemon_message
|
|
340
|
+
: createRelayAgentCallMessage({
|
|
341
|
+
callId,
|
|
342
|
+
caller: msg.caller || "unknown",
|
|
343
|
+
target: agentName,
|
|
344
|
+
task: msg.task || "",
|
|
345
|
+
conversationId: msg.conversation_id,
|
|
346
|
+
memoryScope: msg.memory_scope,
|
|
347
|
+
requiresOwnerApproval: msg.require_owner_approval === true,
|
|
348
|
+
});
|
|
349
|
+
const caller = incomingMessage.source.id || msg.caller || "unknown";
|
|
350
|
+
const task = textFromAkemonMessage(incomingMessage);
|
|
351
|
+
console.log(`[agent_call] Incoming ${incomingMessage.type} from ${caller}: ${task.slice(0, 80)}`);
|
|
318
352
|
// Forward to local MCP as a submit_task call
|
|
319
353
|
const initBody = JSON.stringify({
|
|
320
354
|
jsonrpc: "2.0", id: 1,
|
|
@@ -348,7 +382,13 @@ function handleIncomingAgentCall(ws, msg, localPort) {
|
|
|
348
382
|
const callBody = JSON.stringify({
|
|
349
383
|
jsonrpc: "2.0", id: 2,
|
|
350
384
|
method: "tools/call",
|
|
351
|
-
params: {
|
|
385
|
+
params: {
|
|
386
|
+
name: "submit_task",
|
|
387
|
+
arguments: {
|
|
388
|
+
task,
|
|
389
|
+
require_human: incomingMessage.permissions.requiresOwnerApproval === true,
|
|
390
|
+
},
|
|
391
|
+
},
|
|
352
392
|
});
|
|
353
393
|
return doRequest(callBody, sid);
|
|
354
394
|
})
|
|
@@ -377,20 +417,39 @@ function handleIncomingAgentCall(ws, msg, localPort) {
|
|
|
377
417
|
}
|
|
378
418
|
}
|
|
379
419
|
catch { /* use raw */ }
|
|
420
|
+
const responseMessage = createRelayAgentCallResultMessage({
|
|
421
|
+
callId,
|
|
422
|
+
responder: agentName,
|
|
423
|
+
requester: incomingMessage.source,
|
|
424
|
+
result,
|
|
425
|
+
conversationId: incomingMessage.conversationId,
|
|
426
|
+
inReplyTo: incomingMessage.id,
|
|
427
|
+
});
|
|
380
428
|
ws.send(JSON.stringify({
|
|
381
429
|
type: "agent_call_result",
|
|
382
430
|
call_id: callId,
|
|
383
431
|
caller,
|
|
384
432
|
result,
|
|
433
|
+
akemon_message: responseMessage,
|
|
385
434
|
}));
|
|
386
435
|
console.log(`[agent_call] Replied to ${caller} (${result.length} bytes)`);
|
|
387
436
|
})
|
|
388
437
|
.catch((err) => {
|
|
438
|
+
const errorResult = `[error] ${err.message}`;
|
|
439
|
+
const responseMessage = createRelayAgentCallResultMessage({
|
|
440
|
+
callId,
|
|
441
|
+
responder: agentName,
|
|
442
|
+
requester: incomingMessage.source,
|
|
443
|
+
result: errorResult,
|
|
444
|
+
conversationId: incomingMessage.conversationId,
|
|
445
|
+
inReplyTo: incomingMessage.id,
|
|
446
|
+
});
|
|
389
447
|
ws.send(JSON.stringify({
|
|
390
448
|
type: "agent_call_result",
|
|
391
449
|
call_id: callId,
|
|
392
450
|
caller,
|
|
393
|
-
result:
|
|
451
|
+
result: errorResult,
|
|
452
|
+
akemon_message: responseMessage,
|
|
394
453
|
}));
|
|
395
454
|
});
|
|
396
455
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const DEFAULT_RELAY_WS = "wss://relay.akemon.dev";
|
|
2
|
+
export const DEFAULT_RELAY_HTTP = "https://relay.akemon.dev";
|
|
3
|
+
export function relayHttpFromWs(relayWs) {
|
|
4
|
+
return relayWs.replace(/^wss:/, "https:").replace(/^ws:/, "http:");
|
|
5
|
+
}
|
|
6
|
+
export function resolveServeRelayMode(input) {
|
|
7
|
+
const relay = input.relay?.trim();
|
|
8
|
+
if (input.localOnly) {
|
|
9
|
+
if (input.public) {
|
|
10
|
+
throw new Error("--local-only cannot be combined with --public");
|
|
11
|
+
}
|
|
12
|
+
if (relay) {
|
|
13
|
+
throw new Error("--local-only cannot be combined with --relay");
|
|
14
|
+
}
|
|
15
|
+
return { enabled: false, reason: "local-only" };
|
|
16
|
+
}
|
|
17
|
+
if (relay) {
|
|
18
|
+
return {
|
|
19
|
+
enabled: true,
|
|
20
|
+
relayWs: relay,
|
|
21
|
+
relayHttp: relayHttpFromWs(relay),
|
|
22
|
+
reason: "explicit-relay",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (input.public) {
|
|
26
|
+
return {
|
|
27
|
+
enabled: true,
|
|
28
|
+
relayWs: DEFAULT_RELAY_WS,
|
|
29
|
+
relayHttp: DEFAULT_RELAY_HTTP,
|
|
30
|
+
reason: "public-default-relay",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return { enabled: false, reason: "local-only" };
|
|
34
|
+
}
|