totemlayer 0.1.21 → 0.1.25
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 +34 -32
- package/bin/agent-runner.mjs +27 -2
- package/bin/agent-setup.mjs +33 -6
- package/bin/automatic-runtime.mjs +105 -9
- package/bin/bound-login.mjs +107 -0
- package/bin/connected-agent-service.mjs +119 -78
- package/bin/device-login.mjs +108 -9
- package/bin/installation.mjs +175 -76
- package/bin/mission-agent-service.mjs +23 -5
- package/bin/mission-coordination.mjs +311 -0
- package/bin/mission-worker.mjs +271 -20
- package/bin/model-adapter.mjs +30 -8
- package/bin/network-runtime.mjs +115 -15
- package/bin/runtime-diagnostics.mjs +4 -0
- package/bin/runtime-installation.mjs +1 -0
- package/bin/sdk/action-types.d.ts +236 -0
- package/bin/sdk/actions.d.ts +19 -0
- package/bin/sdk/errors.d.ts +30 -0
- package/bin/sdk/errors.js +14 -1
- package/bin/sdk/index.d.ts +300 -0
- package/bin/sdk/index.js +57 -11
- package/bin/sdk/inspector.d.ts +40 -0
- package/bin/sdk/request-options.d.ts +13 -0
- package/bin/sdk/transport.d.ts +24 -0
- package/bin/sdk/types.d.ts +640 -0
- package/bin/totem.mjs +2 -2
- package/package.json +1 -1
- package/scripts/prepare.mjs +9 -20
|
@@ -1,91 +1,132 @@
|
|
|
1
1
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
2
|
import { runMissionAgentService } from "./mission-agent-service.mjs";
|
|
3
3
|
import { runRuntime } from "./network-runtime.mjs";
|
|
4
|
+
import { acquireMissionAgentSession } from "./mission-agent-session.mjs";
|
|
4
5
|
|
|
5
6
|
// Both queues use the same verified identity and private profile. A stopped or
|
|
6
7
|
// revoked connection cancels both consumers; Missions retains its own leases.
|
|
7
8
|
export async function runConnectedAgentService(settings, dependencies = {}) {
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
9
|
+
// Own the agent across both consumers before starting a second profile's probe.
|
|
10
|
+
const session =
|
|
11
|
+
settings.verifyReadiness && settings.client && settings.expectedAgentId
|
|
12
|
+
? await acquireMissionAgentSession(
|
|
13
|
+
{ baseUrl: settings.baseUrl, agentId: settings.expectedAgentId, signal: settings.signal },
|
|
14
|
+
settings.sessionLockDirectory ? { lockDirectory: settings.sessionLockDirectory } : {},
|
|
15
|
+
)
|
|
16
|
+
: null;
|
|
17
|
+
try {
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
const signal = settings.signal
|
|
20
|
+
? AbortSignal.any([settings.signal, controller.signal])
|
|
21
|
+
: controller.signal;
|
|
22
|
+
const emit = settings.emit ?? (() => {});
|
|
23
|
+
const phases = {
|
|
24
|
+
messages: "starting",
|
|
25
|
+
missions: settings.connectionOnly ? "waiting" : "starting",
|
|
26
|
+
};
|
|
27
|
+
let releaseMissions;
|
|
28
|
+
const messageReady = new Promise((resolve) => {
|
|
29
|
+
releaseMissions = resolve;
|
|
30
|
+
});
|
|
31
|
+
signal.addEventListener("abort", () => releaseMissions(false), { once: true });
|
|
32
|
+
if (signal.aborted) releaseMissions(false);
|
|
33
|
+
const report = (queue, event) => {
|
|
34
|
+
if (
|
|
35
|
+
[
|
|
36
|
+
"briefing_updated",
|
|
37
|
+
"file_overlap",
|
|
38
|
+
"session_visibility_unavailable",
|
|
39
|
+
"briefing_unavailable",
|
|
40
|
+
].includes(event.type)
|
|
41
|
+
) {
|
|
42
|
+
emit(event);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
phases[queue] = event.type === "completed" ? "waiting" : event.type;
|
|
46
|
+
const values = Object.values(phases);
|
|
47
|
+
const type =
|
|
48
|
+
["reconnecting", "starting", "blocked", "working"].find((phase) =>
|
|
49
|
+
values.includes(phase),
|
|
50
|
+
) ?? "waiting";
|
|
51
|
+
emit({ ...event, type });
|
|
52
|
+
};
|
|
53
|
+
const messages = async () => {
|
|
54
|
+
let attempts = 0;
|
|
55
|
+
while (!signal.aborted) {
|
|
56
|
+
try {
|
|
57
|
+
await (dependencies.runRuntime ?? runRuntime)({
|
|
58
|
+
...settings,
|
|
59
|
+
signal,
|
|
60
|
+
stateDir: settings.stateDirectory,
|
|
61
|
+
allowAgents: [],
|
|
62
|
+
modelTimeoutMs: 900000,
|
|
63
|
+
replyTimeoutMs: 90000,
|
|
64
|
+
pollMs: settings.pollMs ?? 5000,
|
|
65
|
+
emit(event) {
|
|
66
|
+
if (event.type === "runtime_ready") emit(event);
|
|
67
|
+
else if (event.type === "runtime_reconnecting")
|
|
68
|
+
report("messages", { type: "reconnecting" });
|
|
69
|
+
else if (event.type === "runtime_resumed") report("messages", { type: "waiting" });
|
|
70
|
+
else if (event.type === "runtime_started") {
|
|
71
|
+
releaseMissions(true);
|
|
72
|
+
attempts = 0;
|
|
73
|
+
report("messages", { type: "waiting" });
|
|
74
|
+
} else if (event.type === "runtime_error")
|
|
75
|
+
report("messages", { type: "blocked", code: event.error?.code });
|
|
76
|
+
else if (["message_handled", "chat_completed"].includes(event.type))
|
|
77
|
+
report("messages", { type: "waiting" });
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
if (!signal.aborted) throw new Error("The message runtime stopped unexpectedly.");
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (signal.aborted) return;
|
|
83
|
+
if (
|
|
84
|
+
error.status === 401 ||
|
|
85
|
+
error.status === 403 ||
|
|
86
|
+
["READINESS_PROBE_FAILED", "READINESS_UNSUPPORTED", "RUNTIME_SUPERSEDED"].includes(
|
|
87
|
+
error.code,
|
|
88
|
+
) ||
|
|
89
|
+
error.code === "AGENT_IDENTITY_MISMATCH"
|
|
90
|
+
)
|
|
91
|
+
throw error;
|
|
92
|
+
report("messages", { type: "reconnecting" });
|
|
93
|
+
await sleep(
|
|
94
|
+
error.status === 429 ? 30000 : Math.min(30000, 1000 * 2 ** Math.min(attempts++, 5)),
|
|
95
|
+
undefined,
|
|
96
|
+
{ signal },
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const guarded = async (run) => {
|
|
29
102
|
try {
|
|
30
|
-
await (
|
|
31
|
-
...settings,
|
|
32
|
-
signal,
|
|
33
|
-
stateDir: settings.stateDirectory,
|
|
34
|
-
allowAgents: [],
|
|
35
|
-
adapterSettings: { ...settings.adapterSettings, diagnosticDirectory: settings.diagnosticDirectory },
|
|
36
|
-
modelTimeoutMs: 900000,
|
|
37
|
-
replyTimeoutMs: 90000,
|
|
38
|
-
pollMs: settings.pollMs ?? 5000,
|
|
39
|
-
emit(event) {
|
|
40
|
-
if (event.type === "runtime_started") {
|
|
41
|
-
attempts = 0;
|
|
42
|
-
report("messages", { type: "waiting" });
|
|
43
|
-
} else if (event.type === "runtime_error")
|
|
44
|
-
report("messages", { type: "blocked", code: event.error?.code, messageId: event.messageId });
|
|
45
|
-
else if (event.type === "message_started")
|
|
46
|
-
report("messages", { type: "working", messageId: event.data?.messageId });
|
|
47
|
-
else if (["message_handled", "chat_completed", "chat_queued"].includes(event.type))
|
|
48
|
-
report("messages", { type: "waiting" });
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
if (!signal.aborted) throw new Error("The message runtime stopped unexpectedly.");
|
|
103
|
+
await run();
|
|
52
104
|
} catch (error) {
|
|
53
|
-
if (signal.aborted)
|
|
54
|
-
|
|
55
|
-
error.status === 401 ||
|
|
56
|
-
error.status === 403 ||
|
|
57
|
-
error.code === "AGENT_IDENTITY_MISMATCH"
|
|
58
|
-
)
|
|
105
|
+
if (!signal.aborted) {
|
|
106
|
+
controller.abort();
|
|
59
107
|
throw error;
|
|
60
|
-
|
|
61
|
-
await sleep(
|
|
62
|
-
error.status === 429 ? 30000 : Math.min(30000, 1000 * 2 ** Math.min(attempts++, 5)),
|
|
63
|
-
undefined,
|
|
64
|
-
{ signal },
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
const guarded = async (run) => {
|
|
70
|
-
try {
|
|
71
|
-
await run();
|
|
72
|
-
} catch (error) {
|
|
73
|
-
if (!signal.aborted) {
|
|
74
|
-
controller.abort();
|
|
75
|
-
throw error;
|
|
108
|
+
}
|
|
76
109
|
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
110
|
+
};
|
|
111
|
+
const results = await Promise.allSettled([
|
|
112
|
+
guarded(messages),
|
|
113
|
+
guarded(async () => {
|
|
114
|
+
// Do not dispatch business work while onboarding is still testing its adapter.
|
|
115
|
+
if (settings.connectionOnly) return;
|
|
116
|
+
if (settings.verifyReadiness && !(await messageReady)) return;
|
|
117
|
+
return (dependencies.runMissions ?? runMissionAgentService)(
|
|
118
|
+
{
|
|
119
|
+
...settings,
|
|
120
|
+
signal,
|
|
121
|
+
emit: (event) => report("missions", event),
|
|
122
|
+
},
|
|
123
|
+
session ? { ownedSession: session } : {},
|
|
124
|
+
);
|
|
86
125
|
}),
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
126
|
+
]);
|
|
127
|
+
const failure = results.find((result) => result.status === "rejected");
|
|
128
|
+
if (failure) throw failure.reason;
|
|
129
|
+
} finally {
|
|
130
|
+
await session?.release();
|
|
131
|
+
}
|
|
91
132
|
}
|
package/bin/device-login.mjs
CHANGED
|
@@ -2,10 +2,11 @@ import { hostname } from "node:os";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
|
-
import { privateRead, privateWrite } from "./runtime-installation.mjs";
|
|
5
|
+
import { privateRead, privateWrite, privateDirectory } from "./runtime-installation.mjs";
|
|
6
|
+
import { mkdir, rm, rename } from "node:fs/promises";
|
|
6
7
|
import { detectInstalledProvider, providerArguments } from "./provider-detection.mjs";
|
|
7
8
|
|
|
8
|
-
function origin(value) {
|
|
9
|
+
export function origin(value) {
|
|
9
10
|
const url = new URL(value ?? "https://totemlayer.com");
|
|
10
11
|
if (
|
|
11
12
|
url.username ||
|
|
@@ -42,7 +43,9 @@ export async function beginDeviceLogin(runtime, env = process.env, options = {})
|
|
|
42
43
|
!pending ||
|
|
43
44
|
pending.baseUrl !== baseUrl ||
|
|
44
45
|
pending.requestedAgentId !== env.TOTEM_AGENT_ID ||
|
|
46
|
+
pending.connectionId !== env.TOTEM_CONNECTION_ID ||
|
|
45
47
|
pending.provider !== (env.TOTEM_PROVIDER ?? "auto") ||
|
|
48
|
+
pending.redirectUri !== options.binding?.redirectUri ||
|
|
46
49
|
new Date(pending.expiresAt) <= new Date()
|
|
47
50
|
) {
|
|
48
51
|
const result = await request(
|
|
@@ -55,6 +58,13 @@ export async function beginDeviceLogin(runtime, env = process.env, options = {})
|
|
|
55
58
|
deviceName: hostname().slice(0, 100),
|
|
56
59
|
provider: env.TOTEM_PROVIDER ?? "auto",
|
|
57
60
|
...(env.TOTEM_AGENT_ID ? { agentId: env.TOTEM_AGENT_ID } : {}),
|
|
61
|
+
...(env.TOTEM_CONNECTION_ID ? { connectionId: env.TOTEM_CONNECTION_ID } : {}),
|
|
62
|
+
...(options.binding
|
|
63
|
+
? {
|
|
64
|
+
bindingChallenge: options.binding.challenge,
|
|
65
|
+
redirectUri: options.binding.redirectUri,
|
|
66
|
+
}
|
|
67
|
+
: {}),
|
|
58
68
|
}),
|
|
59
69
|
},
|
|
60
70
|
options.fetch,
|
|
@@ -70,11 +80,18 @@ export async function beginDeviceLogin(runtime, env = process.env, options = {})
|
|
|
70
80
|
...result,
|
|
71
81
|
baseUrl,
|
|
72
82
|
requestedAgentId: env.TOTEM_AGENT_ID,
|
|
83
|
+
connectionId: env.TOTEM_CONNECTION_ID,
|
|
73
84
|
provider: env.TOTEM_PROVIDER ?? "auto",
|
|
85
|
+
replace: Boolean(options.replace),
|
|
86
|
+
...(options.binding
|
|
87
|
+
? { redirectUri: options.binding.redirectUri, verifier: options.binding.verifier }
|
|
88
|
+
: {}),
|
|
74
89
|
};
|
|
75
90
|
await privateWrite(join(runtime.directory, "connection-request.json"), pending);
|
|
76
91
|
}
|
|
77
|
-
const url =
|
|
92
|
+
const url = pending.connectionId
|
|
93
|
+
? `${baseUrl}/connections/${pending.connectionId}/accept?code=${pending.code}`
|
|
94
|
+
: `${baseUrl}/connect?code=${pending.code}`;
|
|
78
95
|
if (options.openBrowser !== false) {
|
|
79
96
|
const command =
|
|
80
97
|
process.platform === "darwin"
|
|
@@ -86,10 +103,45 @@ export async function beginDeviceLogin(runtime, env = process.env, options = {})
|
|
|
86
103
|
() => {},
|
|
87
104
|
);
|
|
88
105
|
}
|
|
89
|
-
return { url, code: pending.code, expiresAt: pending.expiresAt };
|
|
106
|
+
return { id: pending.id, url, code: pending.code, expiresAt: pending.expiresAt };
|
|
90
107
|
}
|
|
91
108
|
|
|
92
109
|
export async function resumeDeviceLogin(runtime, env = process.env, options = {}) {
|
|
110
|
+
await privateDirectory(runtime.directory);
|
|
111
|
+
const lock = join(runtime.directory, "login.lock");
|
|
112
|
+
try {
|
|
113
|
+
await mkdir(lock, { mode: 0o700 });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error.code !== "EEXIST") throw error;
|
|
116
|
+
const owner = await privateRead(join(lock, "owner.json")).catch(() => null);
|
|
117
|
+
if (owner && Number.isSafeInteger(owner.pid) && owner.pid > 0) {
|
|
118
|
+
try {
|
|
119
|
+
process.kill(owner.pid, 0);
|
|
120
|
+
} catch (failure) {
|
|
121
|
+
if (failure.code === "ESRCH") await rm(lock, { recursive: true });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { state: "setup_in_progress" };
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
await privateWrite(join(lock, "owner.json"), { pid: process.pid });
|
|
128
|
+
if (options.reset) {
|
|
129
|
+
for (const name of ["connection-ready.json", "connection-request.json"])
|
|
130
|
+
await rename(
|
|
131
|
+
join(runtime.directory, name),
|
|
132
|
+
join(runtime.directory, `${name}.reauthorize-${Date.now()}`),
|
|
133
|
+
).catch((error) => {
|
|
134
|
+
if (error.code !== "ENOENT") throw error;
|
|
135
|
+
});
|
|
136
|
+
return { state: "reset" };
|
|
137
|
+
}
|
|
138
|
+
return await resumeLoginLocked(runtime, env, options);
|
|
139
|
+
} finally {
|
|
140
|
+
await rm(lock, { recursive: true });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function resumeLoginLocked(runtime, env, options) {
|
|
93
145
|
let staged;
|
|
94
146
|
try {
|
|
95
147
|
staged = await privateRead(join(runtime.directory, "connection-ready.json"));
|
|
@@ -111,6 +163,8 @@ export async function resumeDeviceLogin(runtime, env = process.env, options = {}
|
|
|
111
163
|
{ headers: { "x-totem-installation-token": pending.token } },
|
|
112
164
|
options.fetch,
|
|
113
165
|
);
|
|
166
|
+
if (result.status === "expired")
|
|
167
|
+
throw new Error("This installer authorization expired. Run totem login again.");
|
|
114
168
|
if (result.status !== "approved") return;
|
|
115
169
|
if (
|
|
116
170
|
!/^[a-f0-9-]{36}$/.test(result.agentId) ||
|
|
@@ -119,9 +173,12 @@ export async function resumeDeviceLogin(runtime, env = process.env, options = {}
|
|
|
119
173
|
throw new Error("Totem returned an invalid connection.");
|
|
120
174
|
staged = {
|
|
121
175
|
agentId: result.agentId,
|
|
176
|
+
...(result.connectionId ? { connectionId: result.connectionId } : {}),
|
|
122
177
|
key: result.key,
|
|
123
178
|
baseUrl: pending.baseUrl,
|
|
124
179
|
requestId: pending.id,
|
|
180
|
+
installationId: pending.id,
|
|
181
|
+
replace: Boolean(pending.replace),
|
|
125
182
|
token: pending.token,
|
|
126
183
|
};
|
|
127
184
|
// Persist privately before acknowledging, so an interrupted setup never loses the key.
|
|
@@ -140,8 +197,36 @@ export async function resumeDeviceLogin(runtime, env = process.env, options = {}
|
|
|
140
197
|
delete staged.token;
|
|
141
198
|
await privateWrite(join(runtime.directory, "connection-ready.json"), staged);
|
|
142
199
|
}
|
|
143
|
-
const
|
|
144
|
-
|
|
200
|
+
const reportFailure = async (errorCode) => {
|
|
201
|
+
const pending = await privateRead(join(runtime.directory, "connection-request.json")).catch(
|
|
202
|
+
() => null,
|
|
203
|
+
);
|
|
204
|
+
if (pending && pending.id === staged.installationId)
|
|
205
|
+
await request(
|
|
206
|
+
origin(pending.baseUrl),
|
|
207
|
+
`/api/installations/${pending.id}/runtime`,
|
|
208
|
+
{
|
|
209
|
+
method: "POST",
|
|
210
|
+
headers: {
|
|
211
|
+
"content-type": "application/json",
|
|
212
|
+
"x-totem-installation-token": pending.token,
|
|
213
|
+
},
|
|
214
|
+
body: JSON.stringify({ errorCode }),
|
|
215
|
+
},
|
|
216
|
+
options.fetch,
|
|
217
|
+
).catch(() => {});
|
|
218
|
+
};
|
|
219
|
+
let provider;
|
|
220
|
+
try {
|
|
221
|
+
provider = await (options.detectProvider ?? detectInstalledProvider)(env);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
await reportFailure("UNSUPPORTED_ADAPTER");
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
if (!provider) {
|
|
227
|
+
await reportFailure("PROVIDER_AUTH_REQUIRED");
|
|
228
|
+
return { state: "executor_needed" };
|
|
229
|
+
}
|
|
145
230
|
const { agentSetupMain } = await import("./agent-setup.mjs");
|
|
146
231
|
const status = await (options.setup ?? agentSetupMain)(
|
|
147
232
|
[
|
|
@@ -152,6 +237,8 @@ export async function resumeDeviceLogin(runtime, env = process.env, options = {}
|
|
|
152
237
|
origin(staged.baseUrl),
|
|
153
238
|
...(await providerArguments(provider, env)),
|
|
154
239
|
"--no-start",
|
|
240
|
+
...(staged.connectionId ? ["--no-mcp"] : []),
|
|
241
|
+
...(options.replace || staged.replace ? ["--replace"] : []),
|
|
155
242
|
"--json",
|
|
156
243
|
],
|
|
157
244
|
{
|
|
@@ -160,9 +247,21 @@ export async function resumeDeviceLogin(runtime, env = process.env, options = {}
|
|
|
160
247
|
stderr: { write() {} },
|
|
161
248
|
},
|
|
162
249
|
);
|
|
163
|
-
if (status)
|
|
164
|
-
|
|
250
|
+
if (status) {
|
|
251
|
+
await reportFailure("LISTENER_START_FAILED");
|
|
252
|
+
throw new Error("The approved connection could not be saved.");
|
|
253
|
+
}
|
|
254
|
+
await privateWrite(join(runtime.directory, "connection-last.json"), {
|
|
255
|
+
requestId: staged.installationId,
|
|
256
|
+
profile: staged.agentId,
|
|
257
|
+
});
|
|
165
258
|
await rm(join(runtime.directory, "connection-ready.json"));
|
|
166
|
-
await
|
|
259
|
+
const current = await privateRead(join(runtime.directory, "connection-request.json")).catch(
|
|
260
|
+
(error) => {
|
|
261
|
+
if (error.code !== "ENOENT") throw error;
|
|
262
|
+
},
|
|
263
|
+
);
|
|
264
|
+
if (current?.id === staged.installationId)
|
|
265
|
+
await rm(join(runtime.directory, "connection-request.json"), { force: true });
|
|
167
266
|
return { state: "connected", profile: staged.agentId };
|
|
168
267
|
}
|