totemlayer 0.1.21 → 0.1.24
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 +29 -29
- package/bin/agent-runner.mjs +26 -2
- package/bin/agent-setup.mjs +21 -3
- package/bin/automatic-runtime.mjs +44 -6
- package/bin/bound-login.mjs +107 -0
- package/bin/connected-agent-service.mjs +115 -78
- package/bin/device-login.mjs +100 -8
- package/bin/installation.mjs +129 -74
- 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 +98 -9
- 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 +298 -0
- package/bin/sdk/index.js +55 -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 +638 -0
- package/bin/totem.mjs +2 -2
- package/package.json +1 -1
- package/scripts/prepare.mjs +9 -20
|
@@ -1,91 +1,128 @@
|
|
|
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 = { messages: "starting", missions: "starting" };
|
|
24
|
+
let releaseMissions;
|
|
25
|
+
const messageReady = new Promise((resolve) => {
|
|
26
|
+
releaseMissions = resolve;
|
|
27
|
+
});
|
|
28
|
+
signal.addEventListener("abort", () => releaseMissions(false), { once: true });
|
|
29
|
+
if (signal.aborted) releaseMissions(false);
|
|
30
|
+
const report = (queue, event) => {
|
|
31
|
+
if (
|
|
32
|
+
[
|
|
33
|
+
"briefing_updated",
|
|
34
|
+
"file_overlap",
|
|
35
|
+
"session_visibility_unavailable",
|
|
36
|
+
"briefing_unavailable",
|
|
37
|
+
].includes(event.type)
|
|
38
|
+
) {
|
|
39
|
+
emit(event);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
phases[queue] = event.type === "completed" ? "waiting" : event.type;
|
|
43
|
+
const values = Object.values(phases);
|
|
44
|
+
const type =
|
|
45
|
+
["reconnecting", "starting", "blocked", "working"].find((phase) =>
|
|
46
|
+
values.includes(phase),
|
|
47
|
+
) ?? "waiting";
|
|
48
|
+
emit({ ...event, type });
|
|
49
|
+
};
|
|
50
|
+
const messages = async () => {
|
|
51
|
+
let attempts = 0;
|
|
52
|
+
while (!signal.aborted) {
|
|
53
|
+
try {
|
|
54
|
+
await (dependencies.runRuntime ?? runRuntime)({
|
|
55
|
+
...settings,
|
|
56
|
+
signal,
|
|
57
|
+
stateDir: settings.stateDirectory,
|
|
58
|
+
allowAgents: [],
|
|
59
|
+
modelTimeoutMs: 900000,
|
|
60
|
+
replyTimeoutMs: 90000,
|
|
61
|
+
pollMs: settings.pollMs ?? 5000,
|
|
62
|
+
emit(event) {
|
|
63
|
+
if (event.type === "runtime_ready") emit(event);
|
|
64
|
+
else if (event.type === "runtime_reconnecting")
|
|
65
|
+
report("messages", { type: "reconnecting" });
|
|
66
|
+
else if (event.type === "runtime_resumed") report("messages", { type: "waiting" });
|
|
67
|
+
else if (event.type === "runtime_started") {
|
|
68
|
+
releaseMissions(true);
|
|
69
|
+
attempts = 0;
|
|
70
|
+
report("messages", { type: "waiting" });
|
|
71
|
+
} else if (event.type === "runtime_error")
|
|
72
|
+
report("messages", { type: "blocked", code: event.error?.code });
|
|
73
|
+
else if (["message_handled", "chat_completed"].includes(event.type))
|
|
74
|
+
report("messages", { type: "waiting" });
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
if (!signal.aborted) throw new Error("The message runtime stopped unexpectedly.");
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (signal.aborted) return;
|
|
80
|
+
if (
|
|
81
|
+
error.status === 401 ||
|
|
82
|
+
error.status === 403 ||
|
|
83
|
+
["READINESS_PROBE_FAILED", "READINESS_UNSUPPORTED", "RUNTIME_SUPERSEDED"].includes(
|
|
84
|
+
error.code,
|
|
85
|
+
) ||
|
|
86
|
+
error.code === "AGENT_IDENTITY_MISMATCH"
|
|
87
|
+
)
|
|
88
|
+
throw error;
|
|
89
|
+
report("messages", { type: "reconnecting" });
|
|
90
|
+
await sleep(
|
|
91
|
+
error.status === 429 ? 30000 : Math.min(30000, 1000 * 2 ** Math.min(attempts++, 5)),
|
|
92
|
+
undefined,
|
|
93
|
+
{ signal },
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const guarded = async (run) => {
|
|
29
99
|
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.");
|
|
100
|
+
await run();
|
|
52
101
|
} catch (error) {
|
|
53
|
-
if (signal.aborted)
|
|
54
|
-
|
|
55
|
-
error.status === 401 ||
|
|
56
|
-
error.status === 403 ||
|
|
57
|
-
error.code === "AGENT_IDENTITY_MISMATCH"
|
|
58
|
-
)
|
|
102
|
+
if (!signal.aborted) {
|
|
103
|
+
controller.abort();
|
|
59
104
|
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;
|
|
105
|
+
}
|
|
76
106
|
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
107
|
+
};
|
|
108
|
+
const results = await Promise.allSettled([
|
|
109
|
+
guarded(messages),
|
|
110
|
+
guarded(async () => {
|
|
111
|
+
// Do not dispatch business work while onboarding is still testing its adapter.
|
|
112
|
+
if (settings.verifyReadiness && !(await messageReady)) return;
|
|
113
|
+
return (dependencies.runMissions ?? runMissionAgentService)(
|
|
114
|
+
{
|
|
115
|
+
...settings,
|
|
116
|
+
signal,
|
|
117
|
+
emit: (event) => report("missions", event),
|
|
118
|
+
},
|
|
119
|
+
session ? { ownedSession: session } : {},
|
|
120
|
+
);
|
|
86
121
|
}),
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
122
|
+
]);
|
|
123
|
+
const failure = results.find((result) => result.status === "rejected");
|
|
124
|
+
if (failure) throw failure.reason;
|
|
125
|
+
} finally {
|
|
126
|
+
await session?.release();
|
|
127
|
+
}
|
|
91
128
|
}
|
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 ||
|
|
@@ -43,6 +44,7 @@ export async function beginDeviceLogin(runtime, env = process.env, options = {})
|
|
|
43
44
|
pending.baseUrl !== baseUrl ||
|
|
44
45
|
pending.requestedAgentId !== env.TOTEM_AGENT_ID ||
|
|
45
46
|
pending.provider !== (env.TOTEM_PROVIDER ?? "auto") ||
|
|
47
|
+
pending.redirectUri !== options.binding?.redirectUri ||
|
|
46
48
|
new Date(pending.expiresAt) <= new Date()
|
|
47
49
|
) {
|
|
48
50
|
const result = await request(
|
|
@@ -55,6 +57,12 @@ export async function beginDeviceLogin(runtime, env = process.env, options = {})
|
|
|
55
57
|
deviceName: hostname().slice(0, 100),
|
|
56
58
|
provider: env.TOTEM_PROVIDER ?? "auto",
|
|
57
59
|
...(env.TOTEM_AGENT_ID ? { agentId: env.TOTEM_AGENT_ID } : {}),
|
|
60
|
+
...(options.binding
|
|
61
|
+
? {
|
|
62
|
+
bindingChallenge: options.binding.challenge,
|
|
63
|
+
redirectUri: options.binding.redirectUri,
|
|
64
|
+
}
|
|
65
|
+
: {}),
|
|
58
66
|
}),
|
|
59
67
|
},
|
|
60
68
|
options.fetch,
|
|
@@ -71,6 +79,10 @@ export async function beginDeviceLogin(runtime, env = process.env, options = {})
|
|
|
71
79
|
baseUrl,
|
|
72
80
|
requestedAgentId: env.TOTEM_AGENT_ID,
|
|
73
81
|
provider: env.TOTEM_PROVIDER ?? "auto",
|
|
82
|
+
replace: Boolean(options.replace),
|
|
83
|
+
...(options.binding
|
|
84
|
+
? { redirectUri: options.binding.redirectUri, verifier: options.binding.verifier }
|
|
85
|
+
: {}),
|
|
74
86
|
};
|
|
75
87
|
await privateWrite(join(runtime.directory, "connection-request.json"), pending);
|
|
76
88
|
}
|
|
@@ -86,10 +98,45 @@ export async function beginDeviceLogin(runtime, env = process.env, options = {})
|
|
|
86
98
|
() => {},
|
|
87
99
|
);
|
|
88
100
|
}
|
|
89
|
-
return { url, code: pending.code, expiresAt: pending.expiresAt };
|
|
101
|
+
return { id: pending.id, url, code: pending.code, expiresAt: pending.expiresAt };
|
|
90
102
|
}
|
|
91
103
|
|
|
92
104
|
export async function resumeDeviceLogin(runtime, env = process.env, options = {}) {
|
|
105
|
+
await privateDirectory(runtime.directory);
|
|
106
|
+
const lock = join(runtime.directory, "login.lock");
|
|
107
|
+
try {
|
|
108
|
+
await mkdir(lock, { mode: 0o700 });
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error.code !== "EEXIST") throw error;
|
|
111
|
+
const owner = await privateRead(join(lock, "owner.json")).catch(() => null);
|
|
112
|
+
if (owner && Number.isSafeInteger(owner.pid) && owner.pid > 0) {
|
|
113
|
+
try {
|
|
114
|
+
process.kill(owner.pid, 0);
|
|
115
|
+
} catch (failure) {
|
|
116
|
+
if (failure.code === "ESRCH") await rm(lock, { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { state: "setup_in_progress" };
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
await privateWrite(join(lock, "owner.json"), { pid: process.pid });
|
|
123
|
+
if (options.reset) {
|
|
124
|
+
for (const name of ["connection-ready.json", "connection-request.json"])
|
|
125
|
+
await rename(
|
|
126
|
+
join(runtime.directory, name),
|
|
127
|
+
join(runtime.directory, `${name}.reauthorize-${Date.now()}`),
|
|
128
|
+
).catch((error) => {
|
|
129
|
+
if (error.code !== "ENOENT") throw error;
|
|
130
|
+
});
|
|
131
|
+
return { state: "reset" };
|
|
132
|
+
}
|
|
133
|
+
return await resumeLoginLocked(runtime, env, options);
|
|
134
|
+
} finally {
|
|
135
|
+
await rm(lock, { recursive: true });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function resumeLoginLocked(runtime, env, options) {
|
|
93
140
|
let staged;
|
|
94
141
|
try {
|
|
95
142
|
staged = await privateRead(join(runtime.directory, "connection-ready.json"));
|
|
@@ -111,6 +158,8 @@ export async function resumeDeviceLogin(runtime, env = process.env, options = {}
|
|
|
111
158
|
{ headers: { "x-totem-installation-token": pending.token } },
|
|
112
159
|
options.fetch,
|
|
113
160
|
);
|
|
161
|
+
if (result.status === "expired")
|
|
162
|
+
throw new Error("This installer authorization expired. Run totem login again.");
|
|
114
163
|
if (result.status !== "approved") return;
|
|
115
164
|
if (
|
|
116
165
|
!/^[a-f0-9-]{36}$/.test(result.agentId) ||
|
|
@@ -122,6 +171,8 @@ export async function resumeDeviceLogin(runtime, env = process.env, options = {}
|
|
|
122
171
|
key: result.key,
|
|
123
172
|
baseUrl: pending.baseUrl,
|
|
124
173
|
requestId: pending.id,
|
|
174
|
+
installationId: pending.id,
|
|
175
|
+
replace: Boolean(pending.replace),
|
|
125
176
|
token: pending.token,
|
|
126
177
|
};
|
|
127
178
|
// Persist privately before acknowledging, so an interrupted setup never loses the key.
|
|
@@ -140,8 +191,36 @@ export async function resumeDeviceLogin(runtime, env = process.env, options = {}
|
|
|
140
191
|
delete staged.token;
|
|
141
192
|
await privateWrite(join(runtime.directory, "connection-ready.json"), staged);
|
|
142
193
|
}
|
|
143
|
-
const
|
|
144
|
-
|
|
194
|
+
const reportFailure = async (errorCode) => {
|
|
195
|
+
const pending = await privateRead(join(runtime.directory, "connection-request.json")).catch(
|
|
196
|
+
() => null,
|
|
197
|
+
);
|
|
198
|
+
if (pending && pending.id === staged.installationId)
|
|
199
|
+
await request(
|
|
200
|
+
origin(pending.baseUrl),
|
|
201
|
+
`/api/installations/${pending.id}/runtime`,
|
|
202
|
+
{
|
|
203
|
+
method: "POST",
|
|
204
|
+
headers: {
|
|
205
|
+
"content-type": "application/json",
|
|
206
|
+
"x-totem-installation-token": pending.token,
|
|
207
|
+
},
|
|
208
|
+
body: JSON.stringify({ errorCode }),
|
|
209
|
+
},
|
|
210
|
+
options.fetch,
|
|
211
|
+
).catch(() => {});
|
|
212
|
+
};
|
|
213
|
+
let provider;
|
|
214
|
+
try {
|
|
215
|
+
provider = await (options.detectProvider ?? detectInstalledProvider)(env);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
await reportFailure("UNSUPPORTED_ADAPTER");
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
if (!provider) {
|
|
221
|
+
await reportFailure("PROVIDER_AUTH_REQUIRED");
|
|
222
|
+
return { state: "executor_needed" };
|
|
223
|
+
}
|
|
145
224
|
const { agentSetupMain } = await import("./agent-setup.mjs");
|
|
146
225
|
const status = await (options.setup ?? agentSetupMain)(
|
|
147
226
|
[
|
|
@@ -152,6 +231,7 @@ export async function resumeDeviceLogin(runtime, env = process.env, options = {}
|
|
|
152
231
|
origin(staged.baseUrl),
|
|
153
232
|
...(await providerArguments(provider, env)),
|
|
154
233
|
"--no-start",
|
|
234
|
+
...(options.replace || staged.replace ? ["--replace"] : []),
|
|
155
235
|
"--json",
|
|
156
236
|
],
|
|
157
237
|
{
|
|
@@ -160,9 +240,21 @@ export async function resumeDeviceLogin(runtime, env = process.env, options = {}
|
|
|
160
240
|
stderr: { write() {} },
|
|
161
241
|
},
|
|
162
242
|
);
|
|
163
|
-
if (status)
|
|
164
|
-
|
|
243
|
+
if (status) {
|
|
244
|
+
await reportFailure("LISTENER_START_FAILED");
|
|
245
|
+
throw new Error("The approved connection could not be saved.");
|
|
246
|
+
}
|
|
247
|
+
await privateWrite(join(runtime.directory, "connection-last.json"), {
|
|
248
|
+
requestId: staged.installationId,
|
|
249
|
+
profile: staged.agentId,
|
|
250
|
+
});
|
|
165
251
|
await rm(join(runtime.directory, "connection-ready.json"));
|
|
166
|
-
await
|
|
252
|
+
const current = await privateRead(join(runtime.directory, "connection-request.json")).catch(
|
|
253
|
+
(error) => {
|
|
254
|
+
if (error.code !== "ENOENT") throw error;
|
|
255
|
+
},
|
|
256
|
+
);
|
|
257
|
+
if (current?.id === staged.installationId)
|
|
258
|
+
await rm(join(runtime.directory, "connection-request.json"), { force: true });
|
|
167
259
|
return { state: "connected", profile: staged.agentId };
|
|
168
260
|
}
|