totemlayer 0.1.44 → 0.1.63
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/README.md +48 -11
- package/bin/agent-memory.mjs +255 -0
- package/bin/agent-profile.mjs +42 -5
- package/bin/agent-runner.mjs +55 -12
- package/bin/agent-setup.mjs +136 -50
- package/bin/automatic-runtime.mjs +26 -7
- package/bin/chat-prompt-limit.mjs +67 -0
- package/bin/codex-chat-bridge.mjs +36 -8
- package/bin/codex-chat-readiness.mjs +7 -0
- package/bin/codex-human-chat-bridge.mjs +209 -0
- package/bin/collaboration-listener.mjs +2 -1
- package/bin/connected-agent-service.mjs +4 -1
- package/bin/inbox-queue.mjs +2 -1
- package/bin/inference-broker.mjs +361 -0
- package/bin/installation.mjs +92 -60
- package/bin/isolated-agent-entry.mjs +35 -0
- package/bin/local-mcp.mjs +43 -4
- package/bin/mcp-config.d.mts +11 -0
- package/bin/mcp-config.mjs +56 -0
- package/bin/mcp-install.mjs +143 -12
- package/bin/mcp-listener.d.mts +22 -0
- package/bin/mcp-listener.mjs +4 -0
- package/bin/mcp-runtime.mjs +3 -1
- package/bin/mcp.mjs +331 -94
- package/bin/mission-agent-service.mjs +2 -1
- package/bin/mission-worker.mjs +13 -2
- package/bin/model-adapter.mjs +245 -107
- package/bin/network-contract.mjs +254 -0
- package/bin/network-runtime.mjs +162 -47
- package/bin/network.mjs +32 -7
- package/bin/platform-client.mjs +25 -3
- package/bin/process-isolation.mjs +214 -0
- package/bin/provider-detection.mjs +1 -4
- package/bin/runtime-diagnostics.mjs +1 -0
- package/bin/sdk/index.d.ts +50 -6
- package/bin/sdk/index.js +1 -1
- package/bin/sdk/inspector.js +1 -1
- package/bin/sdk/listener.d.ts +57 -0
- package/bin/sdk/listener.js +1 -0
- package/bin/sdk/types.d.ts +62 -0
- package/bin/startup-diagnostics.mjs +168 -0
- package/bin/totem.mjs +17 -3
- package/bin/worker-core/adapters.mjs +30 -13
- package/bin/worker-core/errors.mjs +4 -0
- package/package.json +1 -1
- package/scripts/prepare.mjs +28 -8
package/bin/installation.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { startupDiagnostic } from "./startup-diagnostics.mjs";
|
|
1
2
|
import { parseArgs } from "node:util";
|
|
2
3
|
import { readFile, stat } from "node:fs/promises";
|
|
3
4
|
import { privateRead, privateWrite } from "./runtime-installation.mjs";
|
|
@@ -13,72 +14,103 @@ import { setTimeout as sleep } from "node:timers/promises";
|
|
|
13
14
|
import { detectInstalledProvider, providerArguments } from "./provider-detection.mjs";
|
|
14
15
|
|
|
15
16
|
export async function activateConnection(profile, env = process.env, options = {}) {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
});
|
|
24
|
-
if (!enabled) return { state: "paused", ...service, entry: runtime.cli };
|
|
25
|
-
if (profile.interactiveRequired && !service.persistent)
|
|
26
|
-
return {
|
|
27
|
-
state: "needs_attention",
|
|
28
|
-
ready: false,
|
|
29
|
-
...service,
|
|
30
|
-
entry: runtime.cli,
|
|
31
|
-
code: "HARNESS_SETUP_REQUIRED",
|
|
32
|
-
message:
|
|
33
|
-
"Automatic startup is not registered. Incoming work remains queued until host setup succeeds.",
|
|
34
|
-
};
|
|
35
|
-
const { verifyExecutorReady } = await import("./executor-readiness.mjs");
|
|
17
|
+
const health = await privateRead(join(profile.stateDirectory, "automatic-health.json")).catch(
|
|
18
|
+
(error) => {
|
|
19
|
+
if (error.code !== "ENOENT") throw error;
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
if (health?.retryable === false)
|
|
23
|
+
return { state: "needs_attention", ready: false, ...startupDiagnostic(health) };
|
|
36
24
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
25
|
+
const runtime = await (options.ensureRuntime ?? ensureInstalledRuntime)(env);
|
|
26
|
+
const { automaticEnabled } = await import("./automatic-runtime.mjs");
|
|
27
|
+
const enabled = await automaticEnabled(profile);
|
|
28
|
+
|
|
29
|
+
const service = await (options.installService ?? installRuntimeService)(runtime, {
|
|
30
|
+
...env,
|
|
31
|
+
...(profile.agentTokenEnv ? { TOTEM_AGENT_TOKEN_ENV: profile.agentTokenEnv } : {}),
|
|
32
|
+
});
|
|
33
|
+
if (!enabled) return { state: "paused", ...service, entry: runtime.cli };
|
|
34
|
+
if (profile.interactiveRequired && !service.persistent)
|
|
35
|
+
return {
|
|
36
|
+
state: "needs_attention",
|
|
37
|
+
ready: false,
|
|
38
|
+
...service,
|
|
39
|
+
entry: runtime.cli,
|
|
40
|
+
code: "HARNESS_SETUP_REQUIRED",
|
|
41
|
+
message:
|
|
42
|
+
"Automatic startup is not registered. Incoming work remains queued until host setup succeeds.",
|
|
43
|
+
};
|
|
44
|
+
const { verifyExecutorReady } = await import("./executor-readiness.mjs");
|
|
45
|
+
try {
|
|
46
|
+
if (profile.codexThreadId)
|
|
47
|
+
await (options.verifyExecutor ?? verifyExecutorReady)(profile, env);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (!startupDiagnostic(error).retryable) throw error;
|
|
50
|
+
return {
|
|
51
|
+
state: "needs_attention",
|
|
52
|
+
...service,
|
|
53
|
+
entry: runtime.cli,
|
|
54
|
+
code: error.code ?? "EXECUTOR_NOT_READY",
|
|
55
|
+
...startupDiagnostic(error),
|
|
56
|
+
message:
|
|
57
|
+
"Your connection is saved, but the selected agent could not complete its startup check. Run totem status to inspect the blocker.",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (profile.interactiveRequired) {
|
|
61
|
+
const { confirmHarnessStartup } = await import("./harness-bootstrap.mjs");
|
|
62
|
+
await confirmHarnessStartup(profile);
|
|
63
|
+
}
|
|
64
|
+
const start =
|
|
65
|
+
options.start ??
|
|
66
|
+
(
|
|
67
|
+
await import(
|
|
68
|
+
pathToFileURL(
|
|
69
|
+
join(
|
|
70
|
+
runtime.root,
|
|
71
|
+
"runtime-packages",
|
|
72
|
+
runtime.version,
|
|
73
|
+
"node_modules",
|
|
74
|
+
"totemlayer",
|
|
75
|
+
"bin",
|
|
76
|
+
"agent-runner.mjs",
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
).agentStartMain;
|
|
81
|
+
const result = await start(["--profile", profile.profileName, "--background", "--json"], {
|
|
82
|
+
env,
|
|
83
|
+
automatic: true,
|
|
84
|
+
stdout: { write() {} },
|
|
85
|
+
});
|
|
86
|
+
if (result) {
|
|
87
|
+
const failure = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
|
|
88
|
+
throw Object.assign(new Error("The listener could not start."), failure);
|
|
89
|
+
}
|
|
90
|
+
const status = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
|
|
39
91
|
return {
|
|
40
|
-
state: "needs_attention",
|
|
92
|
+
state: status.ready ? "ready" : "needs_attention",
|
|
93
|
+
ready: Boolean(status.ready),
|
|
41
94
|
...service,
|
|
42
95
|
entry: runtime.cli,
|
|
43
|
-
code: error.code ?? "EXECUTOR_NOT_READY",
|
|
44
|
-
message:
|
|
45
|
-
"Your connection is saved, but the selected agent could not complete its startup check. Run totem status to inspect the blocker.",
|
|
46
96
|
};
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const diagnostic = startupDiagnostic(error);
|
|
99
|
+
if (!diagnostic.retryable)
|
|
100
|
+
await privateWrite(join(profile.stateDirectory, "automatic-health.json"), {
|
|
101
|
+
state: "needs_attention",
|
|
102
|
+
code: "LISTENER_START_FAILED",
|
|
103
|
+
...diagnostic,
|
|
104
|
+
retryAt: null,
|
|
105
|
+
updatedAt: new Date().toISOString(),
|
|
106
|
+
});
|
|
107
|
+
throw Object.assign(
|
|
108
|
+
new Error(
|
|
109
|
+
`The listener could not start. ${diagnostic.cause} Recovery: ${diagnostic.recovery}`,
|
|
110
|
+
),
|
|
111
|
+
{ ...diagnostic, cause: error },
|
|
112
|
+
);
|
|
47
113
|
}
|
|
48
|
-
if (profile.interactiveRequired) {
|
|
49
|
-
const { confirmHarnessStartup } = await import("./harness-bootstrap.mjs");
|
|
50
|
-
await confirmHarnessStartup(profile);
|
|
51
|
-
}
|
|
52
|
-
const start =
|
|
53
|
-
options.start ??
|
|
54
|
-
(
|
|
55
|
-
await import(
|
|
56
|
-
pathToFileURL(
|
|
57
|
-
join(
|
|
58
|
-
runtime.root,
|
|
59
|
-
"runtime-packages",
|
|
60
|
-
runtime.version,
|
|
61
|
-
"node_modules",
|
|
62
|
-
"totemlayer",
|
|
63
|
-
"bin",
|
|
64
|
-
"agent-runner.mjs",
|
|
65
|
-
),
|
|
66
|
-
)
|
|
67
|
-
)
|
|
68
|
-
).agentStartMain;
|
|
69
|
-
const result = await start(["--profile", profile.profileName, "--background", "--json"], {
|
|
70
|
-
env,
|
|
71
|
-
automatic: true,
|
|
72
|
-
stdout: { write() {} },
|
|
73
|
-
});
|
|
74
|
-
if (result) throw new Error("The listener could not start. Check totem status for this profile.");
|
|
75
|
-
const status = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
|
|
76
|
-
return {
|
|
77
|
-
state: status.ready ? "ready" : "needs_attention",
|
|
78
|
-
ready: Boolean(status.ready),
|
|
79
|
-
...service,
|
|
80
|
-
entry: runtime.cli,
|
|
81
|
-
};
|
|
82
114
|
}
|
|
83
115
|
|
|
84
116
|
export async function installationMain(args = [], options = {}) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Linux's private network namespace can reach only this relay and the parent broker socket.
|
|
2
|
+
import { createServer } from "node:net";
|
|
3
|
+
import { createConnection } from "node:net";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const config = JSON.parse(process.env.AGENT_SESSION_CONFIG);
|
|
7
|
+
const server = createServer();
|
|
8
|
+
const sockets = new Set();
|
|
9
|
+
server.on("connection", (socket) => {
|
|
10
|
+
sockets.add(socket);
|
|
11
|
+
const parent = createConnection(config.socketPath);
|
|
12
|
+
socket.pipe(parent).pipe(socket);
|
|
13
|
+
parent.on("error", () => socket.destroy());
|
|
14
|
+
socket.on("error", () => parent.destroy());
|
|
15
|
+
socket.on("close", () => {
|
|
16
|
+
sockets.delete(socket);
|
|
17
|
+
parent.destroy();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
await new Promise((resolve) => server.listen(16443, "127.0.0.1", resolve));
|
|
21
|
+
const env = { ...process.env };
|
|
22
|
+
delete env.AGENT_SESSION_CONFIG;
|
|
23
|
+
const child = spawn(config.command, config.args, {
|
|
24
|
+
env,
|
|
25
|
+
cwd: config.cwd,
|
|
26
|
+
stdio: "inherit",
|
|
27
|
+
shell: false,
|
|
28
|
+
});
|
|
29
|
+
const stop = (code) => {
|
|
30
|
+
process.exitCode = code ?? 1;
|
|
31
|
+
for (const socket of sockets) socket.destroy();
|
|
32
|
+
server.close();
|
|
33
|
+
};
|
|
34
|
+
child.on("error", () => stop(1));
|
|
35
|
+
child.on("exit", stop);
|
package/bin/local-mcp.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
1
|
+
import { serveStdio, StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
2
2
|
import * as z from "zod/v4";
|
|
3
|
+
import { Totem } from "./sdk/index.js";
|
|
4
|
+
import { createAutomaticMcpListener } from "./mcp-listener.mjs";
|
|
5
|
+
import { createMcpIdentityGuard } from "./mcp-identity-binding.mjs";
|
|
3
6
|
|
|
4
7
|
import {
|
|
5
8
|
managedCollaborationStatus,
|
|
@@ -76,25 +79,54 @@ export async function mcpMain(environment = process.env) {
|
|
|
76
79
|
try {
|
|
77
80
|
const config = await loadTotemMcpConfig(environment);
|
|
78
81
|
let listener = { state: "checking", ready: false };
|
|
82
|
+
const client = new Totem(config);
|
|
83
|
+
const transport = new StdioServerTransport();
|
|
84
|
+
const automaticEnabled = async () => {
|
|
85
|
+
const status = await (
|
|
86
|
+
await import("./mcp-runtime.mjs")
|
|
87
|
+
).ensureMcpListener(config, environment, {
|
|
88
|
+
keyFile: environment.TOTEM_NETWORK_API_KEY_FILE?.trim(),
|
|
89
|
+
connectionOnly: true,
|
|
90
|
+
});
|
|
91
|
+
return ["setup_required", "managed"].includes(status.state);
|
|
92
|
+
};
|
|
79
93
|
const registerListener = (server, client, register) => {
|
|
80
94
|
register(
|
|
81
95
|
server,
|
|
82
96
|
"totem_get_listener_status",
|
|
83
97
|
{
|
|
84
|
-
title: "Check this
|
|
98
|
+
title: "Check this agent host’s Totem listener",
|
|
85
99
|
description:
|
|
86
100
|
"Show local listener setup, explicit stop, login recovery and readiness. MCP connectivity alone is not receive/wake/reply readiness and grants no work permission.",
|
|
87
101
|
inputSchema: z.object({}).strict(),
|
|
88
102
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
89
103
|
},
|
|
90
104
|
async () =>
|
|
91
|
-
(
|
|
105
|
+
automatic?.status().listenerReady
|
|
106
|
+
? automatic.status()
|
|
107
|
+
: (await import("./mcp-runtime.mjs")).readMcpListenerStatus(
|
|
108
|
+
config,
|
|
109
|
+
environment,
|
|
110
|
+
listener,
|
|
111
|
+
),
|
|
92
112
|
);
|
|
93
113
|
registerLocalCollaborationTools(server, client, register, {
|
|
94
114
|
...config,
|
|
95
115
|
localKeyFile: environment.TOTEM_NETWORK_API_KEY_FILE?.trim(),
|
|
96
116
|
});
|
|
97
117
|
};
|
|
118
|
+
const automatic = createAutomaticMcpListener({
|
|
119
|
+
client,
|
|
120
|
+
authorize: createMcpIdentityGuard({ client, ...config }),
|
|
121
|
+
enabled: automaticEnabled,
|
|
122
|
+
hostKind: environment.TOTEM_MCP_HOST_KIND === "hosted" ? "hosted" : "local",
|
|
123
|
+
notify: (logger, data) =>
|
|
124
|
+
transport.send({
|
|
125
|
+
jsonrpc: "2.0",
|
|
126
|
+
method: "notifications/message",
|
|
127
|
+
params: { level: "info", logger, data },
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
98
130
|
serveStdio(
|
|
99
131
|
() =>
|
|
100
132
|
createTotemMcpServer({
|
|
@@ -103,8 +135,15 @@ export async function mcpMain(environment = process.env) {
|
|
|
103
135
|
localKeyFile: environment.TOTEM_NETWORK_API_KEY_FILE?.trim(),
|
|
104
136
|
registerLocalTools: registerListener,
|
|
105
137
|
}),
|
|
106
|
-
{ onerror: (error) => console.error(`Totem MCP: ${error.message}`) },
|
|
138
|
+
{ transport, onerror: (error) => console.error(`Totem MCP: ${error.message}`) },
|
|
107
139
|
);
|
|
140
|
+
const closed = transport.onclose;
|
|
141
|
+
transport.onclose = () => {
|
|
142
|
+
void automatic.stop();
|
|
143
|
+
closed?.();
|
|
144
|
+
};
|
|
145
|
+
automatic.start();
|
|
146
|
+
process.stdin.once("end", () => void automatic?.stop());
|
|
108
147
|
console.error("Totem MCP server is ready on stdio.");
|
|
109
148
|
// Do not delay the MCP handshake. Reconnect repairs only an already bound
|
|
110
149
|
// profile; creating a host/executor binding remains part of explicit install.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function buildMcpConfig(options?: {
|
|
2
|
+
baseUrl?: string;
|
|
3
|
+
transport?: string;
|
|
4
|
+
format?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
keyFile?: string;
|
|
7
|
+
expectedAgentId?: string;
|
|
8
|
+
expectedEnvironment?: string;
|
|
9
|
+
command?: string;
|
|
10
|
+
args?: string[];
|
|
11
|
+
}): Record<string, Record<string, Record<string, unknown>>>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Connection settings only. No model selection, client detection, or runtime installation. */
|
|
2
|
+
export function buildMcpConfig({
|
|
3
|
+
baseUrl = "https://totemlayer.com",
|
|
4
|
+
transport = "http",
|
|
5
|
+
format = "generic",
|
|
6
|
+
name = "totem",
|
|
7
|
+
keyFile,
|
|
8
|
+
expectedAgentId,
|
|
9
|
+
expectedEnvironment,
|
|
10
|
+
command = "totem",
|
|
11
|
+
args = ["mcp"],
|
|
12
|
+
} = {}) {
|
|
13
|
+
const url = new URL(baseUrl);
|
|
14
|
+
if (
|
|
15
|
+
url.username ||
|
|
16
|
+
url.password ||
|
|
17
|
+
url.search ||
|
|
18
|
+
url.hash ||
|
|
19
|
+
url.pathname !== "/" ||
|
|
20
|
+
(url.protocol !== "https:" &&
|
|
21
|
+
!(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))
|
|
22
|
+
)
|
|
23
|
+
throw new Error("Use a credential-free HTTPS origin, or HTTP loopback for development.");
|
|
24
|
+
if (!["http", "stdio"].includes(transport)) throw new Error("Choose --transport http or stdio.");
|
|
25
|
+
if (!["generic", "muse"].includes(format)) throw new Error("Choose --format generic or muse.");
|
|
26
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(name))
|
|
27
|
+
throw new Error("Choose a valid MCP server name.");
|
|
28
|
+
if (transport === "stdio" && !keyFile) throw new Error("Pass --key-file for a stdio connection.");
|
|
29
|
+
if (expectedAgentId && !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(expectedAgentId))
|
|
30
|
+
throw new Error("Use a valid agent UUID.");
|
|
31
|
+
if (expectedEnvironment && !["PRODUCTION", "SANDBOX"].includes(expectedEnvironment))
|
|
32
|
+
throw new Error("Choose PRODUCTION or SANDBOX.");
|
|
33
|
+
const endpoint = new URL("/mcp", url);
|
|
34
|
+
if (expectedAgentId) endpoint.searchParams.set("agent", expectedAgentId);
|
|
35
|
+
if (expectedEnvironment) endpoint.searchParams.set("environment", expectedEnvironment);
|
|
36
|
+
const server =
|
|
37
|
+
transport === "http"
|
|
38
|
+
? { url: endpoint.toString() }
|
|
39
|
+
: {
|
|
40
|
+
command,
|
|
41
|
+
args,
|
|
42
|
+
env: {
|
|
43
|
+
TOTEM_BASE_URL: url.origin,
|
|
44
|
+
TOTEM_NETWORK_API_KEY_FILE: keyFile,
|
|
45
|
+
...(expectedAgentId ? { TOTEM_EXPECTED_AGENT_ID: expectedAgentId } : {}),
|
|
46
|
+
...(expectedEnvironment ? { TOTEM_EXPECTED_ENVIRONMENT: expectedEnvironment } : {}),
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
return format === "muse"
|
|
50
|
+
? {
|
|
51
|
+
mcp_servers: {
|
|
52
|
+
[name]: { transport: transport === "http" ? "streamable_http" : "stdio", ...server },
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
: { mcpServers: { [name]: { ...(transport === "http" ? { type: "http" } : {}), ...server } } };
|
|
56
|
+
}
|
package/bin/mcp-install.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { buildMcpConfig } from "./mcp-config.mjs";
|
|
1
3
|
import { spawnSync } from "node:child_process";
|
|
2
4
|
import { createHash } from "node:crypto";
|
|
3
5
|
import { stat, lstat } from "node:fs/promises";
|
|
@@ -15,21 +17,33 @@ const SERVER_ENTRY = fileURLToPath(new URL("./totem.mjs", import.meta.url));
|
|
|
15
17
|
export const MCP_INSTALL_HELP = `Connect Totem to an AI app
|
|
16
18
|
|
|
17
19
|
Usage:
|
|
20
|
+
totem mcp install claude Add hosted MCP and open one OAuth approval
|
|
21
|
+
totem mcp install codex Add hosted MCP and open one OAuth approval
|
|
22
|
+
totem mcp install generic HTTP settings for other hosts
|
|
23
|
+
totem mcp config HTTP settings for any MCP client
|
|
24
|
+
totem mcp config --transport stdio --key-file /absolute/path/to/agent.key
|
|
25
|
+
totem mcp config --format muse Muse Code settings
|
|
18
26
|
totem mcp install generic --key-file /absolute/path/to/agent.key
|
|
19
27
|
totem mcp install codex --key-file /absolute/path/to/agent.key
|
|
20
28
|
totem mcp install claude --key-file /absolute/path/to/agent.key
|
|
21
29
|
|
|
22
30
|
Options:
|
|
23
31
|
--base-url <origin> Totem origin (default: https://totemlayer.com)
|
|
24
|
-
--name <name> MCP
|
|
25
|
-
--key-file <path>
|
|
32
|
+
--name <name> MCP name (default: totem, or totem-local with --key-file)
|
|
33
|
+
--key-file <path> Use an existing private agent key for local stdio instead
|
|
26
34
|
--agent-id <UUID> Require this credential-derived agent identity
|
|
27
35
|
--environment <env> Require PRODUCTION or SANDBOX
|
|
28
36
|
--codex-thread <ID> Bind a new Codex listener to this owner-selected chat
|
|
29
37
|
--no-start Configure tools only; do not start a local listener
|
|
30
38
|
--help Show this guide
|
|
31
39
|
|
|
32
|
-
|
|
40
|
+
Without --key-file, this uses the client's native hosted MCP and OAuth support.
|
|
41
|
+
The client saves credentials privately. No Totem website guide, copied key,
|
|
42
|
+
local worker, or model change is required. Existing connections are preserved.
|
|
43
|
+
The host may require reloading tools after adding a server. Call
|
|
44
|
+
totem_get_identity in that host to verify the selected identity and access.
|
|
45
|
+
|
|
46
|
+
With --key-file, this installs a user-level local stdio MCP connection. It never puts the key
|
|
33
47
|
value in a command, client config, or repository. It will not overwrite an
|
|
34
48
|
existing MCP server. Restart the AI app afterward and ask for totem_get_identity.
|
|
35
49
|
Use --agent-id and --environment to reject an unintended agent or environment.
|
|
@@ -82,8 +96,7 @@ function parseInstallArgs(args, environment) {
|
|
|
82
96
|
values[option] = value;
|
|
83
97
|
}
|
|
84
98
|
const keyFile = values["--key-file"] ?? environment.TOTEM_NETWORK_API_KEY_FILE;
|
|
85
|
-
|
|
86
|
-
const name = values["--name"] ?? DEFAULT_NAME;
|
|
99
|
+
const name = values["--name"] ?? (keyFile ? DEFAULT_NAME : "totem");
|
|
87
100
|
if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(name))
|
|
88
101
|
fail(
|
|
89
102
|
"The MCP name must be 1-64 letters, numbers, hyphens or underscores and start with a letter.",
|
|
@@ -118,7 +131,7 @@ function parseInstallArgs(args, environment) {
|
|
|
118
131
|
codexThreadId: values["--codex-thread"],
|
|
119
132
|
configDirectory: environment.TOTEM_CONFIG_DIR ? runtimeRoot(environment) : undefined,
|
|
120
133
|
noStart: values["--no-start"] === true || environment.TOTEM_AUTO_START === "0",
|
|
121
|
-
keyFile: resolve(keyFile),
|
|
134
|
+
keyFile: keyFile ? resolve(keyFile) : undefined,
|
|
122
135
|
name,
|
|
123
136
|
baseUrl: url.origin,
|
|
124
137
|
...loadMcpIdentityBinding({
|
|
@@ -152,16 +165,46 @@ async function validateKeyFile(path, platform) {
|
|
|
152
165
|
fail("The Totem agent key file must be private (mode 0600). Fix its permissions and retry.");
|
|
153
166
|
}
|
|
154
167
|
|
|
168
|
+
export async function configureTotemMcp(args, environment = process.env, runtime = {}) {
|
|
169
|
+
const flags = parseArgs({
|
|
170
|
+
args,
|
|
171
|
+
strict: true,
|
|
172
|
+
options: {
|
|
173
|
+
transport: { type: "string", default: "http" },
|
|
174
|
+
format: { type: "string", default: "generic" },
|
|
175
|
+
name: { type: "string", default: "totem" },
|
|
176
|
+
"base-url": { type: "string" },
|
|
177
|
+
"key-file": { type: "string" },
|
|
178
|
+
"agent-id": { type: "string" },
|
|
179
|
+
environment: { type: "string" },
|
|
180
|
+
help: { type: "boolean", short: "h" },
|
|
181
|
+
},
|
|
182
|
+
}).values;
|
|
183
|
+
if (flags.help) return MCP_INSTALL_HELP;
|
|
184
|
+
const keyFile = flags["key-file"] ?? environment.TOTEM_NETWORK_API_KEY_FILE;
|
|
185
|
+
const config = buildMcpConfig({
|
|
186
|
+
baseUrl: flags["base-url"] ?? environment.TOTEM_BASE_URL ?? DEFAULT_ORIGIN,
|
|
187
|
+
transport: flags.transport,
|
|
188
|
+
format: flags.format,
|
|
189
|
+
name: flags.name,
|
|
190
|
+
keyFile: keyFile ? resolve(keyFile) : undefined,
|
|
191
|
+
expectedAgentId: flags["agent-id"] ?? environment.TOTEM_EXPECTED_AGENT_ID,
|
|
192
|
+
expectedEnvironment: flags.environment ?? environment.TOTEM_EXPECTED_ENVIRONMENT,
|
|
193
|
+
command: process.execPath,
|
|
194
|
+
args: [runtime.serverEntry ?? SERVER_ENTRY, "mcp"],
|
|
195
|
+
});
|
|
196
|
+
if (flags.transport === "stdio")
|
|
197
|
+
await validateKeyFile(resolve(keyFile), runtime.platform ?? process.platform);
|
|
198
|
+
return JSON.stringify(config, null, 2) + "\n";
|
|
199
|
+
}
|
|
200
|
+
|
|
155
201
|
function runClient(command, args, environment, runtime) {
|
|
156
|
-
const invocation =
|
|
157
|
-
runtime.platform === "win32"
|
|
158
|
-
? installedClientInvocation(command, args, environment)
|
|
159
|
-
: { command, args };
|
|
202
|
+
const invocation = installedClientInvocation(command, args, environment, runtime);
|
|
160
203
|
const result = runtime.spawnProcess(invocation.command, invocation.args, {
|
|
161
204
|
encoding: "utf8",
|
|
162
205
|
env: environment,
|
|
163
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
164
|
-
timeout: 20_000,
|
|
206
|
+
stdio: runtime.stdio ?? ["ignore", "pipe", "pipe"],
|
|
207
|
+
timeout: runtime.timeoutMs ?? 20_000,
|
|
165
208
|
});
|
|
166
209
|
if (result.error?.code === "ENOENT")
|
|
167
210
|
fail(
|
|
@@ -171,10 +214,98 @@ function runClient(command, args, environment, runtime) {
|
|
|
171
214
|
return result;
|
|
172
215
|
}
|
|
173
216
|
|
|
217
|
+
function hostedEntryMatches(result, client, endpoint) {
|
|
218
|
+
if (result.status !== 0) return false;
|
|
219
|
+
if (client === "codex") {
|
|
220
|
+
try {
|
|
221
|
+
const entry = JSON.parse(result.stdout);
|
|
222
|
+
const transport = entry.transport;
|
|
223
|
+
return (
|
|
224
|
+
entry.enabled !== false &&
|
|
225
|
+
!entry.disabled_reason &&
|
|
226
|
+
transport?.type === "streamable_http" &&
|
|
227
|
+
transport.url === endpoint &&
|
|
228
|
+
!transport.bearer_token_env_var &&
|
|
229
|
+
Object.keys(transport.http_headers ?? {}).length === 0 &&
|
|
230
|
+
Object.keys(transport.env_http_headers ?? {}).length === 0
|
|
231
|
+
);
|
|
232
|
+
} catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Claude currently exposes human-readable get output rather than a JSON export.
|
|
237
|
+
const output = String(result.stdout ?? "").replace(/\u001b\[[0-9;]*m/g, "");
|
|
238
|
+
const field = (name) => output.match(new RegExp(`^\\s*${name}:\\s*(.*)$`, "mi"))?.[1]?.trim();
|
|
239
|
+
return (
|
|
240
|
+
field("Type") === "http" &&
|
|
241
|
+
field("URL") === endpoint &&
|
|
242
|
+
!/^.*(?:disabled|rejected|pending approval)/im.test(field("Status") ?? "") &&
|
|
243
|
+
!/^\s*(?:Headers?|Authorization|Environment|headersHelper):/im.test(output)
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function installHostedTotemMcp(options, environment, runtime) {
|
|
248
|
+
const config = buildMcpConfig({
|
|
249
|
+
baseUrl: options.baseUrl,
|
|
250
|
+
name: options.name,
|
|
251
|
+
expectedAgentId: options.expectedAgentId,
|
|
252
|
+
expectedEnvironment: options.expectedEnvironment,
|
|
253
|
+
});
|
|
254
|
+
if (options.client === "generic") return JSON.stringify(config, null, 2) + "\n";
|
|
255
|
+
if (options.codexThreadId)
|
|
256
|
+
fail(
|
|
257
|
+
"--codex-thread requires local stdio setup with --key-file. Hosted MCP does not bind a desktop chat.",
|
|
258
|
+
);
|
|
259
|
+
const endpoint = config.mcpServers[options.name].url;
|
|
260
|
+
const env = localClientEnvironment(environment);
|
|
261
|
+
const getArgs = ["mcp", "get", options.name, ...(options.client === "codex" ? ["--json"] : [])];
|
|
262
|
+
const inspect = () => runClient(options.client, getArgs, env, runtime);
|
|
263
|
+
let entry = inspect();
|
|
264
|
+
if (entry.status === 0 && !hostedEntryMatches(entry, options.client, endpoint))
|
|
265
|
+
fail(
|
|
266
|
+
`${options.name} already exists with different or disabled settings. It was preserved; inspect that exact connection before continuing.`,
|
|
267
|
+
);
|
|
268
|
+
if (entry.status !== 0) {
|
|
269
|
+
if (!/no mcp server|not found|does not exist/i.test(`${entry.stderr}\n${entry.stdout}`))
|
|
270
|
+
fail("The client could not check its existing MCP settings. No configuration was changed.");
|
|
271
|
+
const args =
|
|
272
|
+
options.client === "claude"
|
|
273
|
+
? ["mcp", "add", "--transport", "http", "--scope", "user", options.name, endpoint]
|
|
274
|
+
: ["mcp", "add", options.name, "--url", endpoint];
|
|
275
|
+
if (runClient(options.client, args, env, runtime).status !== 0)
|
|
276
|
+
fail(
|
|
277
|
+
"The host could not save Totem's MCP connection. Check its MCP configuration permissions.",
|
|
278
|
+
);
|
|
279
|
+
entry = inspect();
|
|
280
|
+
if (!hostedEntryMatches(entry, options.client, endpoint))
|
|
281
|
+
fail(
|
|
282
|
+
"Totem's saved MCP settings could not be verified. No sign-in was started; inspect the exact entry before retrying.",
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
const connected =
|
|
286
|
+
options.client === "claude" &&
|
|
287
|
+
/^\s*Status:\s*(?:✔\s*)?Connected\s*$/im.test(
|
|
288
|
+
String(entry.stdout ?? "").replace(/\u001b\[[0-9;]*m/g, ""),
|
|
289
|
+
);
|
|
290
|
+
if (!connected) {
|
|
291
|
+
const login = runClient(options.client, ["mcp", "login", options.name], env, {
|
|
292
|
+
...runtime,
|
|
293
|
+
timeoutMs: 600_000,
|
|
294
|
+
stdio: "inherit",
|
|
295
|
+
});
|
|
296
|
+
if (login.status !== 0)
|
|
297
|
+
fail(
|
|
298
|
+
`Totem's settings are saved, but approval is incomplete. Finish this server's native MCP sign-in in ${options.client}; do not create another connection or switch identities. Older hosts may require their MCP sign-in panel.`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
return `Totem's hosted MCP settings are saved in ${options.client}. ${connected ? "The existing authenticated connection was reused." : "The client's OAuth approval completed; credentials stay in its private store."} Reload this host's tools if needed, then call totem_get_identity to verify the agent, organization and environment. Check incoming chat delivery separately.\n`;
|
|
302
|
+
}
|
|
303
|
+
|
|
174
304
|
export async function installTotemMcp(args, environment = process.env, runtime = {}) {
|
|
175
305
|
const options = parseInstallArgs(args, environment);
|
|
176
306
|
if (options.help) return MCP_INSTALL_HELP;
|
|
177
307
|
runtime = { platform: process.platform, spawnProcess: spawnSync, ...runtime };
|
|
308
|
+
if (!options.keyFile) return installHostedTotemMcp(options, environment, runtime);
|
|
178
309
|
await validateKeyFile(options.keyFile, runtime.platform);
|
|
179
310
|
|
|
180
311
|
if (options.client === "generic")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Totem } from "./sdk/index.js";
|
|
2
|
+
export function createAutomaticMcpListener(options: {
|
|
3
|
+
client: Totem;
|
|
4
|
+
authorize?: (signal: AbortSignal) => Promise<unknown>;
|
|
5
|
+
enabled?: () => Promise<boolean>;
|
|
6
|
+
notify: (logger: string, data: unknown, cursor?: string) => Promise<void>;
|
|
7
|
+
hostKind?: "local" | "hosted";
|
|
8
|
+
pollMs?: number;
|
|
9
|
+
cursors?: { cursor?: string; messageCursor?: string };
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
}): {
|
|
12
|
+
start(): void;
|
|
13
|
+
stop(): Promise<void>;
|
|
14
|
+
done(): Promise<void>;
|
|
15
|
+
status(): {
|
|
16
|
+
state: string;
|
|
17
|
+
listenerReady: boolean;
|
|
18
|
+
executionAuthorized: boolean;
|
|
19
|
+
runId?: string;
|
|
20
|
+
code?: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
package/bin/mcp-runtime.mjs
CHANGED
|
@@ -17,7 +17,7 @@ function listenerBinding(profile) {
|
|
|
17
17
|
|
|
18
18
|
/** Recover only the exact credential-bound profile. MCP transport grants no new host authority. */
|
|
19
19
|
export async function ensureMcpListener(config, environment = process.env, options = {}) {
|
|
20
|
-
if (environment.TOTEM_AUTO_START === "0" || environment.CI)
|
|
20
|
+
if (environment.TOTEM_AUTO_START === "0" || (environment.CI && !options.connectionOnly))
|
|
21
21
|
return { state: "disabled_by_request", ready: false, persistent: false };
|
|
22
22
|
if (!options.keyFile)
|
|
23
23
|
return {
|
|
@@ -122,6 +122,8 @@ export async function ensureMcpListener(config, environment = process.env, optio
|
|
|
122
122
|
message:
|
|
123
123
|
"This listener was explicitly stopped. Its opt-out remains in effect; totem start can resume it.",
|
|
124
124
|
};
|
|
125
|
+
if (options.connectionOnly)
|
|
126
|
+
return { state: "managed", ready: false, ...listenerBinding(profile) };
|
|
125
127
|
const activate = options.activate ?? (await import("./installation.mjs")).activateConnection;
|
|
126
128
|
if (["codex", "claude"].includes(profile.client ?? profile.provider)) {
|
|
127
129
|
try {
|