totemlayer 0.1.97 → 0.1.115
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 +21 -6
- package/bin/automatic-runtime.mjs +2 -1
- package/bin/codex-mission-bridge.mjs +6 -2
- package/bin/configured-native-installation.mjs +57 -0
- package/bin/connected-agent-service.mjs +5 -1
- package/bin/device-login.mjs +3 -1
- package/bin/inbox-queue.mjs +8 -5
- package/bin/mcp.mjs +18 -5
- package/bin/mission-agent-service.mjs +13 -4
- package/bin/native/darwin-lifetime +0 -0
- package/bin/native/darwin-lifetime.json +1 -1
- package/bin/native-claude-login.mjs +131 -0
- package/bin/native-claude-receiver.mjs +272 -0
- package/bin/native-cli-process.py +105 -0
- package/bin/native-harness-environment.mjs +3 -1
- package/bin/native-hermes-bridge.mjs +99 -0
- package/bin/native-hermes-installation.mjs +9 -0
- package/bin/native-hermes-mcp.mjs +107 -0
- package/bin/native-hermes-mcp.py +205 -0
- package/bin/native-hermes-metadata.py +40 -0
- package/bin/native-hermes-process.py +62 -0
- package/bin/native-hermes-receiver.mjs +28 -0
- package/bin/native-inbox-installation.mjs +144 -0
- package/bin/native-inbox-receiver.mjs +133 -0
- package/bin/native-kilo-installation.mjs +4 -0
- package/bin/native-kilo-receiver.mjs +1 -0
- package/bin/native-mcp-source.mjs +244 -0
- package/bin/native-opencode-bridge.mjs +108 -0
- package/bin/native-opencode-installation.mjs +15 -0
- package/bin/native-opencode-mcp.mjs +112 -0
- package/bin/native-opencode-receiver.mjs +22 -0
- package/bin/native-pi-bridge.mjs +170 -0
- package/bin/native-pi-installation.mjs +9 -0
- package/bin/native-pi-mcp.mjs +139 -0
- package/bin/native-pi-receiver.mjs +28 -0
- package/bin/network-retry.mjs +9 -1
- package/bin/runtime-installation.mjs +6 -1
- package/bin/sdk/errors.d.ts +1 -1
- package/bin/sdk/errors.js +1 -1
- package/bin/sdk/listener.d.ts +1 -0
- package/bin/sdk/listener.js +1 -1
- package/bin/sdk/transport.js +1 -1
- package/bin/setup-assets/api-manifest.json +1 -1
- package/bin/setup-assets/manifest.json +1 -1
- package/bin/setup-assets/setup.backup.md +2 -2
- package/bin/setup-assets/setup.md +2 -2
- package/bin/startup-diagnostics.mjs +19 -2
- package/bin/strict-installation.mjs +29 -3
- package/bin/supervisor-watchdog.mjs +44 -0
- package/bin/windows-private-path.mjs +227 -32
- package/package.json +2 -2
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { spawn, execFile } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
4
|
+
import { realpath } from "node:fs/promises";
|
|
5
|
+
import { StringDecoder } from "node:string_decoder";
|
|
6
|
+
import { localClientEnvironment } from "./platform-client.mjs";
|
|
7
|
+
|
|
8
|
+
const fail = (code) => Object.assign(new Error(code), { code });
|
|
9
|
+
const uuid = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;
|
|
10
|
+
function supportedPolicyArgs(args) {
|
|
11
|
+
for (let index = 0; index < args.length; index++) {
|
|
12
|
+
const value = args[index];
|
|
13
|
+
if (["--no-extensions", "--no-skills", "--no-prompt-templates", "--no-builtin-tools"].includes(value)) continue;
|
|
14
|
+
if (["-e", "--extension"].includes(value) && args[++index] === "builtin:mcp") continue;
|
|
15
|
+
if (value === "--tools" && /^[A-Za-z0-9_,:.-]+$/.test(args[++index] ?? "")) continue;
|
|
16
|
+
return false; // Never accept a positional startup prompt or an arbitrary extension.
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Setup supplies an owned dedicated session and its observed native settings.
|
|
22
|
+
* Queued messages may supply prompt text, never invocation or binding settings.
|
|
23
|
+
* This bridge proves native execution, not a Totem reply or durable installation.
|
|
24
|
+
*/
|
|
25
|
+
export function createPiNativeBridge(binding, options = {}) {
|
|
26
|
+
if (
|
|
27
|
+
binding?.dedicated !== true || !uuid.test(binding.sessionId ?? "") ||
|
|
28
|
+
![binding.node, binding.cliPath, binding.cwd, binding.sessionFile].every(v => typeof v === "string" && isAbsolute(v) && !/[\0\r\n]/.test(v)) ||
|
|
29
|
+
![binding.provider, binding.model, binding.thinkingLevel].every(v => typeof v === "string" && /^[a-zA-Z0-9._:/-]{1,128}$/.test(v)) ||
|
|
30
|
+
!Array.isArray(binding.args) || binding.args.length > 64 ||
|
|
31
|
+
!binding.args.every(v => typeof v === "string" && v.length < 4096 && !/\0|tn_(?:live|test)_|Bearer\s/i.test(v)) ||
|
|
32
|
+
!supportedPolicyArgs(binding.args)
|
|
33
|
+
) throw fail("NATIVE_PI_BINDING_INVALID");
|
|
34
|
+
const settings = structuredClone(binding);
|
|
35
|
+
const environment = localClientEnvironment(options.env ?? process.env);
|
|
36
|
+
// A background host must use its own saved sign-in. Coordinator credentials
|
|
37
|
+
// could silently select a different provider account or override native OAuth.
|
|
38
|
+
for (const key of Object.keys(environment)) {
|
|
39
|
+
if (/^(TOTEM|AGENTTRUST)_|^(OPENAI_API_KEY|ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN)$/i.test(key)) delete environment[key];
|
|
40
|
+
}
|
|
41
|
+
let child, closing, closed = false, busy = false, turn, buffer = "", sessionPath;
|
|
42
|
+
const pending = new Map(), decoder = new StringDecoder("utf8");
|
|
43
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? 10000;
|
|
44
|
+
|
|
45
|
+
function rejectAll(code) {
|
|
46
|
+
for (const item of pending.values()) { clearTimeout(item.timer); item.reject(fail(code)); }
|
|
47
|
+
pending.clear();
|
|
48
|
+
turn?.finish(fail(code));
|
|
49
|
+
}
|
|
50
|
+
function terminal(code) {
|
|
51
|
+
rejectAll(code);
|
|
52
|
+
void close();
|
|
53
|
+
}
|
|
54
|
+
function accept(record) {
|
|
55
|
+
if (!record || typeof record !== "object" || Array.isArray(record)) return terminal("NATIVE_PI_PROTOCOL_INVALID");
|
|
56
|
+
if (record.type === "response") {
|
|
57
|
+
const item = pending.get(record.id);
|
|
58
|
+
if (!item || item.command !== record.command) return terminal("NATIVE_PI_PROTOCOL_INVALID");
|
|
59
|
+
clearTimeout(item.timer); pending.delete(record.id);
|
|
60
|
+
if (record.success !== true) item.reject(fail("NATIVE_PI_COMMAND_FAILED"));
|
|
61
|
+
else item.resolve(record.data);
|
|
62
|
+
} else if (record.type === "extension_ui_request") {
|
|
63
|
+
terminal("NATIVE_PI_INTERACTION_REQUIRED");
|
|
64
|
+
} else if (record.type === "message_end" && record.message?.role === "assistant" && ["error", "aborted"].includes(record.message.stopReason)) {
|
|
65
|
+
if (turn) turn.failed = true;
|
|
66
|
+
} else if (record.type === "agent_settled" && turn) {
|
|
67
|
+
turn.settled = true;
|
|
68
|
+
if (turn.accepted) turn.finish(turn.failed ? fail("NATIVE_PI_TURN_FAILED") : undefined);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function request(type, values = {}) {
|
|
72
|
+
if (!child || closed || closing || child.exitCode !== null || child.signalCode !== null) return Promise.reject(fail("NATIVE_PI_UNAVAILABLE"));
|
|
73
|
+
const id = randomUUID();
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const timer = setTimeout(() => { terminal("NATIVE_PI_REQUEST_TIMEOUT"); }, requestTimeoutMs);
|
|
76
|
+
pending.set(id, { command: type, resolve, reject, timer });
|
|
77
|
+
child.stdin.write(JSON.stringify({ ...values, type, id }) + "\n", error => {
|
|
78
|
+
if (error && pending.has(id)) { clearTimeout(timer); pending.delete(id); reject(fail("NATIVE_PI_UNAVAILABLE")); }
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async function inspect() {
|
|
83
|
+
const state = await request("get_state");
|
|
84
|
+
const observedPath = typeof state?.sessionFile === "string" ? await realpath(state.sessionFile).catch(() => undefined) : undefined;
|
|
85
|
+
if (state?.sessionId !== settings.sessionId || observedPath !== sessionPath) throw fail("NATIVE_PI_SESSION_CHANGED");
|
|
86
|
+
if (state.model?.provider !== settings.provider || state.model?.id !== settings.model || state.thinkingLevel !== settings.thinkingLevel) throw fail("NATIVE_PI_MODEL_CHANGED");
|
|
87
|
+
return { sessionId: state.sessionId, sessionFile: sessionPath, provider: state.model.provider, model: state.model.id,
|
|
88
|
+
thinkingLevel: state.thinkingLevel, isStreaming: state.isStreaming, pendingMessageCount: state.pendingMessageCount };
|
|
89
|
+
}
|
|
90
|
+
async function close() {
|
|
91
|
+
if (closing) return closing;
|
|
92
|
+
closed = true; rejectAll("NATIVE_PI_CLOSED");
|
|
93
|
+
if (!child) return;
|
|
94
|
+
closing = new Promise(resolve => {
|
|
95
|
+
if (child.exitCode !== null || child.signalCode !== null) return resolve();
|
|
96
|
+
let force;
|
|
97
|
+
const timer = setTimeout(() => {
|
|
98
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
99
|
+
if (process.platform === "win32") {
|
|
100
|
+
execFile("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, timeout: 3000 }, () => {});
|
|
101
|
+
} else child.kill("SIGTERM");
|
|
102
|
+
force = setTimeout(() => {
|
|
103
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
104
|
+
}, 2000);
|
|
105
|
+
}, 2000);
|
|
106
|
+
child.once("close", () => { clearTimeout(timer); clearTimeout(force); resolve(); });
|
|
107
|
+
child.stdin.end();
|
|
108
|
+
});
|
|
109
|
+
return closing;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
async open() {
|
|
113
|
+
if (child || closed) throw fail("NATIVE_PI_ALREADY_OPENED");
|
|
114
|
+
sessionPath = await realpath(settings.sessionFile).catch(() => { throw fail("NATIVE_PI_SESSION_UNAVAILABLE"); });
|
|
115
|
+
try { child = (options.spawn ?? spawn)(settings.node, [settings.cliPath, "--mode", "rpc", ...settings.args,
|
|
116
|
+
"--session", sessionPath, "--provider", settings.provider, "--model", settings.model, "--thinking", settings.thinkingLevel],
|
|
117
|
+
{ cwd: settings.cwd, env: environment, windowsHide: true, stdio: ["pipe", "pipe", "pipe"] }); }
|
|
118
|
+
catch { closed = true; throw fail("NATIVE_PI_START_FAILED"); }
|
|
119
|
+
child.stderr.resume(); // Provider diagnostics never enter returned results or shared logs.
|
|
120
|
+
child.stdin.on("error", () => terminal("NATIVE_PI_UNAVAILABLE"));
|
|
121
|
+
child.once("error", () => rejectAll("NATIVE_PI_START_FAILED"));
|
|
122
|
+
child.once("exit", () => { closed = true; rejectAll("NATIVE_PI_PROCESS_EXITED"); });
|
|
123
|
+
child.stdout.on("data", bytes => {
|
|
124
|
+
buffer += decoder.write(bytes);
|
|
125
|
+
if (Buffer.byteLength(buffer) > 8 * 1024 * 1024) { rejectAll("NATIVE_PI_OUTPUT_LIMIT"); void close(); return; }
|
|
126
|
+
let index;
|
|
127
|
+
while ((index = buffer.indexOf("\n")) >= 0) {
|
|
128
|
+
const line = buffer.slice(0, index).replace(/\r$/, ""); buffer = buffer.slice(index + 1);
|
|
129
|
+
if (!line) continue;
|
|
130
|
+
try { accept(JSON.parse(line)); } catch { rejectAll("NATIVE_PI_PROTOCOL_INVALID"); void close(); }
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
try { return await inspect(); } catch (error) { await close(); throw error; }
|
|
134
|
+
},
|
|
135
|
+
inspect,
|
|
136
|
+
async run(message, { signal, timeoutMs = 120000 } = {}) {
|
|
137
|
+
if (typeof message !== "string" || !message.trim() || Buffer.byteLength(message) > 65536 || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 900000) throw fail("NATIVE_PI_PROMPT_INVALID");
|
|
138
|
+
if (busy) throw fail("NATIVE_PI_BUSY");
|
|
139
|
+
if (signal?.aborted) throw fail("NATIVE_PI_CANCELLED");
|
|
140
|
+
busy = true;
|
|
141
|
+
try {
|
|
142
|
+
const state = await inspect();
|
|
143
|
+
if (state.isStreaming || state.pendingMessageCount) throw fail("NATIVE_PI_BUSY");
|
|
144
|
+
await new Promise((resolve, reject) => {
|
|
145
|
+
const abort = () => { void request("abort").catch(() => {}); active.finish(fail("NATIVE_PI_CANCELLED")); };
|
|
146
|
+
const timer = setTimeout(() => { void request("abort").catch(() => {}); active.finish(fail("NATIVE_PI_TURN_TIMEOUT")); }, timeoutMs);
|
|
147
|
+
const active = { accepted: false, settled: false, failed: false, finish(error) {
|
|
148
|
+
if (turn !== active) return;
|
|
149
|
+
turn = undefined; clearTimeout(timer); signal?.removeEventListener("abort", abort);
|
|
150
|
+
if (error) reject(error); else resolve();
|
|
151
|
+
} };
|
|
152
|
+
turn = active; signal?.addEventListener("abort", abort, { once: true });
|
|
153
|
+
if (signal?.aborted) return abort();
|
|
154
|
+
void request("prompt", { message }).then(data => {
|
|
155
|
+
if (data?.disposition !== "started") return active.finish(fail("NATIVE_PI_WAKE_UNVERIFIED"));
|
|
156
|
+
active.accepted = true;
|
|
157
|
+
if (active.settled) active.finish(active.failed ? fail("NATIVE_PI_TURN_FAILED") : undefined);
|
|
158
|
+
}, () => active.finish(fail("NATIVE_PI_COMMAND_FAILED")));
|
|
159
|
+
});
|
|
160
|
+
return { nativeRunSettled: true, ...await inspect() };
|
|
161
|
+
} catch (error) {
|
|
162
|
+
// RPC events carry no turn identifier. Retiring this process after an
|
|
163
|
+
// uncertain run prevents a late agent_settled from completing new work.
|
|
164
|
+
await close();
|
|
165
|
+
throw error;
|
|
166
|
+
} finally { busy = false; }
|
|
167
|
+
},
|
|
168
|
+
close,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createPiNativeBridge } from "./native-pi-bridge.mjs";
|
|
2
|
+
import { createPiMcpSurface } from "./native-pi-mcp.mjs";
|
|
3
|
+
import { installNativeInboxReceiver } from "./native-inbox-installation.mjs";
|
|
4
|
+
|
|
5
|
+
export function installPiNativeReceiver(root, config, options = {}) {
|
|
6
|
+
return installNativeInboxReceiver(root, config, { kind: "pi", createBridge: createPiNativeBridge, createSurface: createPiMcpSurface,
|
|
7
|
+
files: ["native-pi-bridge.mjs", "native-pi-mcp.mjs", "native-mcp-source.mjs", "native-pi-receiver.mjs", "native-inbox-receiver.mjs"],
|
|
8
|
+
}, options);
|
|
9
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { localClientEnvironment } from "./platform-client.mjs";
|
|
6
|
+
|
|
7
|
+
const fail = code => Object.assign(new Error(code), { code });
|
|
8
|
+
const checkerTools = new Set([
|
|
9
|
+
"totem_get_identity", "totem_start_listener", "totem_listen",
|
|
10
|
+
"totem_list_messages", "totem_list_chat_commands", "totem_get_chat", "totem_get_conversation", "totem_get_activity",
|
|
11
|
+
]);
|
|
12
|
+
function canonical(value) {
|
|
13
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
14
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])]));
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Fingerprints stay local. Neither native configuration nor OAuth state is returned. */
|
|
19
|
+
export function piMcpBindingFingerprint(entry) {
|
|
20
|
+
return createHash("sha256").update(JSON.stringify(canonical({
|
|
21
|
+
name: entry.name, config: entry.config, source: entry.source,
|
|
22
|
+
scope: entry.scope, override: entry.override,
|
|
23
|
+
}))).digest("hex");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function loadNative(root) {
|
|
27
|
+
const load = path => import(pathToFileURL(join(root, "dist", path)).href);
|
|
28
|
+
const [config, runtime, auth] = await Promise.all([
|
|
29
|
+
load("extensions/mcp/config.js"), load("extensions/mcp/runtime.js"), load("core/auth-storage.js"),
|
|
30
|
+
]);
|
|
31
|
+
return { ...config, ...runtime, ...auth };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Uses the installed host's transport and refresh locks, never a copied token.
|
|
35
|
+
* Binding and module loading are supplied by trusted local setup, never messages.
|
|
36
|
+
* Only the fixed inbox-checker tools are callable through this surface.
|
|
37
|
+
*/
|
|
38
|
+
export async function createPiMcpSurface(binding, options = {}) {
|
|
39
|
+
if (!binding || ![binding.nativeRoot, binding.agentDir, binding.cwd].every(value =>
|
|
40
|
+
typeof value === "string" && isAbsolute(value) && !/[\0\r\n]/.test(value)) ||
|
|
41
|
+
!/^[a-zA-Z0-9_-]{1,80}$/.test(binding.serverName ?? "") ||
|
|
42
|
+
!/^[a-f0-9]{64}$/.test(binding.serverFingerprint ?? "") ||
|
|
43
|
+
!/^\d+\.\d+\.\d+(?:[-+][a-zA-Z0-9.-]+)?$/.test(binding.nativeVersion ?? "") ||
|
|
44
|
+
typeof binding.projectTrusted !== "boolean") throw fail("NATIVE_PI_MCP_BINDING_INVALID");
|
|
45
|
+
const saved = structuredClone(binding);
|
|
46
|
+
const environment = localClientEnvironment(options.env ?? process.env);
|
|
47
|
+
for (const key of Object.keys(environment)) {
|
|
48
|
+
if (/^(TOTEM|AGENTTRUST)_|^(OPENAI_API_KEY|ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN)$/i.test(key)) delete environment[key];
|
|
49
|
+
}
|
|
50
|
+
let native, connection, closing, incoming = false, closed = false;
|
|
51
|
+
try {
|
|
52
|
+
const manifest = JSON.parse(await readFile(join(saved.nativeRoot, "package.json"), "utf8"));
|
|
53
|
+
if (manifest.name !== "@earendil-works/pi-coding-agent" || manifest.version !== saved.nativeVersion) throw fail("NATIVE_PI_VERSION_CHANGED");
|
|
54
|
+
native = await (options.loadNative ?? loadNative)(saved.nativeRoot);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw fail(error.code === "NATIVE_PI_VERSION_CHANGED" ? error.code : "NATIVE_PI_MCP_UNAVAILABLE");
|
|
57
|
+
}
|
|
58
|
+
function inspect() {
|
|
59
|
+
if (closed) throw fail("NATIVE_PI_MCP_CLOSED");
|
|
60
|
+
let loaded;
|
|
61
|
+
try { loaded = native.loadMcpConfig({ agentDir: saved.agentDir, cwd: saved.cwd, projectTrusted: saved.projectTrusted }); }
|
|
62
|
+
catch { throw fail("NATIVE_PI_MCP_CONFIG_INVALID"); }
|
|
63
|
+
if (loaded.errors?.length) throw fail("NATIVE_PI_MCP_CONFIG_INVALID");
|
|
64
|
+
const entry = loaded.servers?.find(server => server.name === saved.serverName);
|
|
65
|
+
if (!entry) throw fail("NATIVE_PI_MCP_CONFIG_CHANGED");
|
|
66
|
+
if (entry.config.enabled === false) throw fail("LISTENER_PAUSED");
|
|
67
|
+
if (piMcpBindingFingerprint(entry) !== saved.serverFingerprint) throw fail("NATIVE_PI_MCP_CONFIG_CHANGED");
|
|
68
|
+
// Provider-token forwarding is a distinct grant; this adapter uses MCP OAuth
|
|
69
|
+
// or the owner's already-configured stdio connection only.
|
|
70
|
+
if (entry.config.auth?.provider) throw fail("NATIVE_PI_MCP_AUTH_UNSUPPORTED");
|
|
71
|
+
return entry;
|
|
72
|
+
}
|
|
73
|
+
async function close() {
|
|
74
|
+
closed = true;
|
|
75
|
+
closing ??= Promise.resolve().then(() => connection?.close()).catch(() => { throw fail("NATIVE_PI_MCP_CLOSE_FAILED"); });
|
|
76
|
+
await closing;
|
|
77
|
+
}
|
|
78
|
+
function safeError(error) {
|
|
79
|
+
const known = ["RUN_CANCELLED", "LISTENER_PAUSED", "NATIVE_PI_MCP_CLOSED", "NATIVE_PI_MCP_CONFIG_INVALID", "NATIVE_PI_MCP_CONFIG_CHANGED", "NATIVE_PI_MCP_AUTH_UNSUPPORTED", "MCP_TOOL_UNAVAILABLE"];
|
|
80
|
+
if (known.includes(error.code)) return fail(error.code);
|
|
81
|
+
if (connection?.state === "needs-auth") return fail("AGENT_AUTH_REQUIRED");
|
|
82
|
+
if (error.name === "AbortError" || error.name === "TimeoutError") return fail(error.name === "TimeoutError" ? "TIMEOUT" : "RUN_CANCELLED");
|
|
83
|
+
const result = fail("NATIVE_PI_MCP_CALL_FAILED");
|
|
84
|
+
if (Number.isInteger(error.status) && error.status >= 400 && error.status <= 599) result.status = error.status;
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const entry = inspect();
|
|
89
|
+
connection = new native.McpServerConnection({
|
|
90
|
+
entry, cwd: saved.cwd, createTransport(current, cwd, authProvider) {
|
|
91
|
+
const transport = native.createDefaultTransport(current, cwd, authProvider);
|
|
92
|
+
if ("url" in current.config) {
|
|
93
|
+
incoming = typeof transport?.startGetStream === "function" && typeof transport?.openSseStream === "function";
|
|
94
|
+
return transport;
|
|
95
|
+
}
|
|
96
|
+
if (!transport?.options || transport.constructor === Object || typeof transport.constructor !== "function") throw fail("NATIVE_PI_MCP_UNAVAILABLE");
|
|
97
|
+
// The native transport resolves the owner's explicit server env. Retain
|
|
98
|
+
// that grant while excluding unrelated coordinator process credentials.
|
|
99
|
+
return new transport.constructor({ ...transport.options,
|
|
100
|
+
inheritEnv: false, env: { ...environment, ...transport.options.env },
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
credentials: new native.McpOAuthCredentialStore(
|
|
104
|
+
new native.FileAuthStorageBackend(join(saved.agentDir, "mcp-auth.json")), saved.agentDir,
|
|
105
|
+
),
|
|
106
|
+
onTools() {},
|
|
107
|
+
});
|
|
108
|
+
const connectSignal = AbortSignal.any([...(options.signal ? [options.signal] : []), AbortSignal.timeout(20000)]);
|
|
109
|
+
const cancel = () => { void close().catch(() => {}); };
|
|
110
|
+
connectSignal.addEventListener("abort", cancel, { once: true });
|
|
111
|
+
try {
|
|
112
|
+
if (connectSignal.aborted) throw fail("RUN_CANCELLED");
|
|
113
|
+
await connection.getClient();
|
|
114
|
+
if (connectSignal.aborted) throw fail("RUN_CANCELLED");
|
|
115
|
+
} finally { connectSignal.removeEventListener("abort", cancel); }
|
|
116
|
+
} catch (error) {
|
|
117
|
+
const safe = safeError(error);
|
|
118
|
+
await close().catch(() => {});
|
|
119
|
+
throw safe;
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
get nativeIncoming() { return incoming; },
|
|
123
|
+
inspect: () => { inspect(); return { serverName: saved.serverName, nativeVersion: saved.nativeVersion, connected: connection.state === "connected" }; },
|
|
124
|
+
listTools() {
|
|
125
|
+
const entry = inspect();
|
|
126
|
+
return connection.tools.filter(tool => checkerTools.has(tool.name) && native.getMcpToolExposure(entry.config, tool.name) !== "hidden")
|
|
127
|
+
.map(tool => ({ name: tool.name, inputSchema: structuredClone(tool.inputSchema) }));
|
|
128
|
+
},
|
|
129
|
+
async call(name, args = {}, { signal } = {}) {
|
|
130
|
+
try {
|
|
131
|
+
const entry = inspect();
|
|
132
|
+
if (!checkerTools.has(name) || native.getMcpToolExposure(entry.config, name) === "hidden" ||
|
|
133
|
+
!connection.tools.some(tool => tool.name === name)) throw fail("MCP_TOOL_UNAVAILABLE");
|
|
134
|
+
return await connection.callTool(name, args, { signal });
|
|
135
|
+
} catch (error) { throw safeError(error); }
|
|
136
|
+
},
|
|
137
|
+
close,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { privateRead } from "./runtime-installation.mjs";
|
|
4
|
+
import { createPiNativeBridge } from "./native-pi-bridge.mjs";
|
|
5
|
+
import { createPiMcpSurface } from "./native-pi-mcp.mjs";
|
|
6
|
+
import { runNativeInboxReceiver } from "./native-inbox-receiver.mjs";
|
|
7
|
+
|
|
8
|
+
const fail = code => Object.assign(new Error(code), { code });
|
|
9
|
+
const safe = error => /^[A-Z0-9_]{1,100}$/.test(error?.code ?? "") ? error.code : "NATIVE_PI_RECEIVER_FAILED";
|
|
10
|
+
export function runPiNativeReceiver(config, options = {}) {
|
|
11
|
+
return runNativeInboxReceiver(config, { kind: "pi", createBridge: createPiNativeBridge, createSurface: createPiMcpSurface }, options);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const stop = () => controller.abort();
|
|
17
|
+
process.once("SIGTERM", stop); process.once("SIGINT", stop);
|
|
18
|
+
try {
|
|
19
|
+
if (!process.argv[2]) throw fail("NATIVE_PI_RECEIVER_CONFIG_REQUIRED");
|
|
20
|
+
const result = await runPiNativeReceiver(await privateRead(resolve(process.argv[2])), { signal: controller.signal });
|
|
21
|
+
if (result.state === "paused") {
|
|
22
|
+
// A service must remain quiet while paused, rather than restarting a model
|
|
23
|
+
// or repeatedly reconnecting. Resume is an explicit local setup action.
|
|
24
|
+
await new Promise(resolve => { if (controller.signal.aborted) resolve(); else controller.signal.addEventListener("abort", resolve, { once: true }); });
|
|
25
|
+
}
|
|
26
|
+
} catch (error) { process.stderr.write(safe(error) + "\n"); process.exitCode = 1; }
|
|
27
|
+
finally { process.off("SIGTERM", stop); process.off("SIGINT", stop); }
|
|
28
|
+
}
|
package/bin/network-retry.mjs
CHANGED
|
@@ -8,6 +8,14 @@ export function transientNetworkError(error) {
|
|
|
8
8
|
);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// Server cooldowns are minimum waits, including after SDK retries are exhausted.
|
|
12
|
+
export function networkRetryDelay(error, fallbackMs) {
|
|
13
|
+
const seconds = error?.retryAfterSeconds;
|
|
14
|
+
return Number.isSafeInteger(seconds) && seconds > 0 && seconds <= 86400
|
|
15
|
+
? Math.max(fallbackMs, seconds * 1000)
|
|
16
|
+
: fallbackMs;
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
// Only use for reads or mutations protected by a durable idempotency key.
|
|
12
20
|
export async function retryNetwork(
|
|
13
21
|
operation,
|
|
@@ -20,7 +28,7 @@ export async function retryNetwork(
|
|
|
20
28
|
} catch (error) {
|
|
21
29
|
if (signal?.aborted || !transientNetworkError(error) || attempt + 1 >= attempts) throw error;
|
|
22
30
|
onRetry(error);
|
|
23
|
-
await pause(Math.min(delayMs * 2 ** attempt, 10000), signal);
|
|
31
|
+
await pause(networkRetryDelay(error, Math.min(delayMs * 2 ** attempt, 10000)), signal);
|
|
24
32
|
}
|
|
25
33
|
}
|
|
26
34
|
}
|
|
@@ -223,16 +223,19 @@ if (process.env.TOTEM_RUNTIME_ENV_LOADED === entry) {
|
|
|
223
223
|
stopping = true; controller.abort(); child?.kill(signal);
|
|
224
224
|
}]);
|
|
225
225
|
for (const [signal, handler] of handlers) process.once(signal, handler);
|
|
226
|
+
${target === "automatic-runtime.mjs" ? `const { watchSupervisorChild } = await import(pathToFileURL(join(modulePath, '..', 'supervisor-watchdog.mjs')));` : ""}
|
|
226
227
|
do {
|
|
227
228
|
const started = Date.now();
|
|
228
229
|
child = spawn(process.execPath, [entry, ...process.argv.slice(2)], {
|
|
229
230
|
env: { ...environment, TOTEM_CONFIG_DIR: join(root, '..'), TOTEM_RUNTIME_ENV_LOADED: entry },
|
|
230
231
|
stdio: 'inherit',
|
|
231
232
|
});
|
|
233
|
+
${target === "automatic-runtime.mjs" ? "const stopWatchdog = watchSupervisorChild(child, root);" : ""}
|
|
232
234
|
const code = await new Promise((resolve) => {
|
|
233
235
|
child.once('error', () => resolve(1));
|
|
234
236
|
child.once('exit', (code) => resolve(code ?? 1));
|
|
235
237
|
});
|
|
238
|
+
${target === "automatic-runtime.mjs" ? "stopWatchdog();" : ""}
|
|
236
239
|
${
|
|
237
240
|
target === "automatic-runtime.mjs"
|
|
238
241
|
? `if (code === 0) break;
|
|
@@ -415,7 +418,7 @@ export async function installRuntimeService(runtime, env = process.env, options
|
|
|
415
418
|
if (exact) {
|
|
416
419
|
const signal = options.processSignal ?? process.kill;
|
|
417
420
|
signal(status.pid, "SIGTERM");
|
|
418
|
-
const deadline = Date.now() + 20000;
|
|
421
|
+
const deadline = Date.now() + (options.stopTimeoutMs ?? 20000);
|
|
419
422
|
while (Date.now() < deadline) {
|
|
420
423
|
try {
|
|
421
424
|
signal(status.pid, 0);
|
|
@@ -425,6 +428,8 @@ export async function installRuntimeService(runtime, env = process.env, options
|
|
|
425
428
|
}
|
|
426
429
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
427
430
|
}
|
|
431
|
+
if ((await (options.processIdentity ?? processIdentity)(status.pid, owner.identity)) === owner.identity)
|
|
432
|
+
signal(status.pid, "SIGKILL");
|
|
428
433
|
}
|
|
429
434
|
}
|
|
430
435
|
} catch {
|
package/bin/sdk/errors.d.ts
CHANGED
|
@@ -27,4 +27,4 @@ export declare class TotemNetworkError extends TotemError {
|
|
|
27
27
|
}
|
|
28
28
|
export declare class TotemCanceledError extends TotemError {
|
|
29
29
|
}
|
|
30
|
-
export declare function apiError(status: number, body: unknown, requestId?: string): TotemError;
|
|
30
|
+
export declare function apiError(status: number, body: unknown, requestId?: string, headerRetryAfterSeconds?: number): TotemError;
|
package/bin/sdk/errors.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
class TotemError extends Error{code;status;requestId;retryAfterSeconds;constructor(message,options){super(message,{cause:options.cause});this.name=new.target.name;this.code=options.code;this.status=options.status;this.requestId=options.requestId;this.retryAfterSeconds=options.retryAfterSeconds}}class TotemAuthenticationError extends TotemError{}class TotemPermissionError extends TotemError{}class TotemNotFoundError extends TotemError{}class TotemConflictError extends TotemError{}class TotemRateLimitError extends TotemError{}class TotemTimeoutError extends TotemError{}class TotemNetworkError extends TotemError{}class TotemCanceledError extends TotemError{}function apiError(status,body,requestId){const error=body&&typeof body==="object"&&"error"in body&&body.error&&typeof body.error==="object"?body.error:{};const message="message"in error&&typeof error.message==="string"?error.message:`Totem request failed (${status}).`;const code="code"in error&&typeof error.code==="string"?error.code:"HTTP_ERROR";const id="requestId"in error&&typeof error.requestId==="string"?error.requestId:requestId;const Constructor=status===401?TotemAuthenticationError:status===403?TotemPermissionError:status===404?TotemNotFoundError:status===409?TotemConflictError:status===429?TotemRateLimitError:TotemError;const details="details"in error?error.details:void 0;const retryAfter=details&&typeof details==="object"&&"retryAfterSeconds"in details?details.retryAfterSeconds:void 0;const
|
|
1
|
+
class TotemError extends Error{code;status;requestId;retryAfterSeconds;constructor(message,options){super(message,{cause:options.cause});this.name=new.target.name;this.code=options.code;this.status=options.status;this.requestId=options.requestId;this.retryAfterSeconds=options.retryAfterSeconds}}class TotemAuthenticationError extends TotemError{}class TotemPermissionError extends TotemError{}class TotemNotFoundError extends TotemError{}class TotemConflictError extends TotemError{}class TotemRateLimitError extends TotemError{}class TotemTimeoutError extends TotemError{}class TotemNetworkError extends TotemError{}class TotemCanceledError extends TotemError{}function apiError(status,body,requestId,headerRetryAfterSeconds){const error=body&&typeof body==="object"&&"error"in body&&body.error&&typeof body.error==="object"?body.error:{};const message="message"in error&&typeof error.message==="string"?error.message:`Totem request failed (${status}).`;const code="code"in error&&typeof error.code==="string"?error.code:"HTTP_ERROR";const id="requestId"in error&&typeof error.requestId==="string"?error.requestId:requestId;const Constructor=status===401?TotemAuthenticationError:status===403?TotemPermissionError:status===404?TotemNotFoundError:status===409?TotemConflictError:status===429?TotemRateLimitError:TotemError;const details="details"in error?error.details:void 0;const retryAfter=details&&typeof details==="object"&&"retryAfterSeconds"in details?details.retryAfterSeconds:void 0;const delays=[retryAfter,headerRetryAfterSeconds].filter(value=>[429,503].includes(status)&&typeof value==="number"&&Number.isSafeInteger(value)&&value>0&&value<=86400);const retryAfterSeconds=delays.length?Math.max(...delays):void 0;return new Constructor(message,{code,status,requestId:id,retryAfterSeconds})}export{TotemAuthenticationError,TotemCanceledError,TotemConflictError,TotemError,TotemNetworkError,TotemNotFoundError,TotemPermissionError,TotemRateLimitError,TotemTimeoutError,apiError};
|
package/bin/sdk/listener.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Totem } from "./index.js";
|
|
2
2
|
export declare const DEFAULT_LISTENER_POLL_MS = 5000;
|
|
3
|
+
export declare function listenerRetryDelay(retryAfterSeconds: unknown, pollMs?: number): number;
|
|
3
4
|
export type ListenerStatus = {
|
|
4
5
|
state: string;
|
|
5
6
|
listenerReady: boolean;
|
package/bin/sdk/listener.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{sleep}from"./transport.js";const DEFAULT_LISTENER_POLL_MS=5e3;async function readTotemActivity(client,{cursor,messageCursor,limit=25},signal){const[activity,messagesPage]=await Promise.all([client.activity.list({cursor,limit},{signal}),client.messages.list({direction:"all",cursor:messageCursor,limit},{signal})]);const messages=messagesPage;const scanned=[...messages.data,...messages.blocked??[]];const last=scanned.reduce((value,row)=>BigInt(row.sequence)>BigInt(value)?row.sequence:value,messageCursor??"0");return{activity,messages:{...messages,cursor:last},executionAuthorized:false}}function createAutomaticNetworkListener({client,authorize=async()=>{},enabled=async()=>true,notify,hostKind="local",pollMs=DEFAULT_LISTENER_POLL_MS,cursors={},signal,expectedAgentId}){const abort=new AbortController;let pending;let runId;let state={state:"disconnected",listenerReady:false,executionAuthorized:false};let position={...cursors};const options={signal:abort.signal};signal?.addEventListener("abort",()=>abort.abort(),{once:true});if(signal?.aborted)abort.abort();async function status(next){if(state.state===next.state&&state.runId===next.runId)return;state={listenerReady:false,executionAuthorized:false,...next};await notify("totem.listener",state)}async function loop(){try{if(!await enabled()){await status({state:"paused"});return}while(!abort.signal.aborted){let delay=pollMs;try{if(!await enabled()){await status({state:"paused"});break}await authorize(abort.signal);if(!runId){const identity=await client.agent(options);if(expectedAgentId&&identity.id!==expectedAgentId||!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(identity.id)||!["PRODUCTION","SANDBOX"].includes(identity.environment))throw Object.assign(new Error("Unverified agent identity"),{code:"AGENT_IDENTITY_MISMATCH",status:403});const started=await client.runtime.start({runId:globalThis.crypto.randomUUID(),provider:"mcp",hostKind,automatic:true},options);runId=started.runId;const probe=await client.runtime.probe(runId,options);if(probe.provider!=="mcp")throw Object.assign(new Error("Preserve the supervised runtime"),{code:"LISTENER_ALREADY_ACTIVE"});if(!probe.completed)await client.runtime.replyProbe({runId,reply:`TOTEM_READY:${probe.challenge}`},options)}await client.runtime.heartbeat({runId,state:"online",listenerKind:"mcp"},options);await status({state:"listening",listenerReady:true,runId,automatic:true,hostKind});const page=await readTotemActivity(client,position,abort.signal);if(page.activity.data.length||page.messages.data.length||page.messages.blocked?.length){await notify("totem.activity",page,`${page.activity.cursor}:${page.messages.cursor}`)}position={cursor:page.activity.cursor,messageCursor:page.messages.cursor};if(page.activity.nextCursor||page.messages.nextCursor)delay=0}catch(caught){const error=caught;if(abort.signal.aborted)break;if([401,403].includes(error.status??0)||error.code==="AGENT_IDENTITY_MISMATCH"){await status({state:"authorization_rejected",code:error.code});break}runId=void 0;const paused=["LISTENER_PAUSED","LISTENER_STOPPED"].includes(error.code??"");await status({state:paused?"paused":error.code==="LISTENER_ALREADY_ACTIVE"?"managed":"reconnecting",code:error.code??"NETWORK_ERROR"});delay=
|
|
1
|
+
import{sleep}from"./transport.js";const DEFAULT_LISTENER_POLL_MS=5e3;function listenerRetryDelay(retryAfterSeconds,pollMs=DEFAULT_LISTENER_POLL_MS){const serverDelay=Number.isSafeInteger(retryAfterSeconds)&&retryAfterSeconds>0&&retryAfterSeconds<=86400?retryAfterSeconds*1e3:5e3;return Math.max(pollMs,serverDelay)}async function readTotemActivity(client,{cursor,messageCursor,limit=25},signal){const[activity,messagesPage]=await Promise.all([client.activity.list({cursor,limit},{signal}),client.messages.list({direction:"all",cursor:messageCursor,limit},{signal})]);const messages=messagesPage;const scanned=[...messages.data,...messages.blocked??[]];const last=scanned.reduce((value,row)=>BigInt(row.sequence)>BigInt(value)?row.sequence:value,messageCursor??"0");return{activity,messages:{...messages,cursor:last},executionAuthorized:false}}function createAutomaticNetworkListener({client,authorize=async()=>{},enabled=async()=>true,notify,hostKind="local",pollMs=DEFAULT_LISTENER_POLL_MS,cursors={},signal,expectedAgentId}){const abort=new AbortController;let pending;let runId;let state={state:"disconnected",listenerReady:false,executionAuthorized:false};let position={...cursors};const options={signal:abort.signal};signal?.addEventListener("abort",()=>abort.abort(),{once:true});if(signal?.aborted)abort.abort();async function status(next){if(state.state===next.state&&state.runId===next.runId)return;state={listenerReady:false,executionAuthorized:false,...next};await notify("totem.listener",state)}async function loop(){try{if(!await enabled()){await status({state:"paused"});return}while(!abort.signal.aborted){let delay=pollMs;try{if(!await enabled()){await status({state:"paused"});break}await authorize(abort.signal);if(!runId){const identity=await client.agent(options);if(expectedAgentId&&identity.id!==expectedAgentId||!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(identity.id)||!["PRODUCTION","SANDBOX"].includes(identity.environment))throw Object.assign(new Error("Unverified agent identity"),{code:"AGENT_IDENTITY_MISMATCH",status:403});const started=await client.runtime.start({runId:globalThis.crypto.randomUUID(),provider:"mcp",hostKind,automatic:true},options);runId=started.runId;const probe=await client.runtime.probe(runId,options);if(probe.provider!=="mcp")throw Object.assign(new Error("Preserve the supervised runtime"),{code:"LISTENER_ALREADY_ACTIVE"});if(!probe.completed)await client.runtime.replyProbe({runId,reply:`TOTEM_READY:${probe.challenge}`},options)}await client.runtime.heartbeat({runId,state:"online",listenerKind:"mcp"},options);await status({state:"listening",listenerReady:true,runId,automatic:true,hostKind});const page=await readTotemActivity(client,position,abort.signal);if(page.activity.data.length||page.messages.data.length||page.messages.blocked?.length){await notify("totem.activity",page,`${page.activity.cursor}:${page.messages.cursor}`)}position={cursor:page.activity.cursor,messageCursor:page.messages.cursor};if(page.activity.nextCursor||page.messages.nextCursor)delay=0}catch(caught){const error=caught;if(abort.signal.aborted)break;if([401,403].includes(error.status??0)||error.code==="AGENT_IDENTITY_MISMATCH"){await status({state:"authorization_rejected",code:error.code});break}runId=void 0;const paused=["LISTENER_PAUSED","LISTENER_STOPPED"].includes(error.code??"");await status({state:paused?"paused":error.code==="LISTENER_ALREADY_ACTIVE"?"managed":"reconnecting",code:error.code??"NETWORK_ERROR"});delay=listenerRetryDelay(error.retryAfterSeconds,pollMs)}await sleep(delay,abort.signal)}}catch(caught){const error=caught;if(!abort.signal.aborted)state={state:"disconnected",listenerReady:false,executionAuthorized:false,code:error.code??"MCP_TRANSPORT_CLOSED"}}finally{state={...state,state:["listening","disconnected"].includes(state.state)?"disconnected":state.state,listenerReady:false}}}return{start(){pending??=loop()},async stop(){abort.abort();await pending},status(){return{...state}},async done(){await pending}}}export{DEFAULT_LISTENER_POLL_MS,createAutomaticNetworkListener,listenerRetryDelay,readTotemActivity};
|
package/bin/sdk/transport.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{apiError,TotemCanceledError,TotemError,TotemNetworkError,TotemTimeoutError}from"./errors.js";function idempotencyKey(){return globalThis.crypto.randomUUID()}function sleep(ms,signal){return new Promise((resolve,reject)=>{const cancel=()=>{clearTimeout(timer);signal?.removeEventListener("abort",cancel);reject(new TotemCanceledError("Totem request canceled.",{code:"CANCELED"}))};const timer=setTimeout(()=>{signal?.removeEventListener("abort",cancel);resolve()},ms);signal?.addEventListener("abort",cancel,{once:true});if(signal?.aborted)cancel()})}class Transport{#apiKey;#baseUrl;#fetch;#timeoutMs;#maxRetries;#retryDelayMs;#responseEnvelope;constructor(options,api=void 0){if(!options.apiKey?.trim())throw new TotemError("An agent API key is required.",{code:"INVALID_CONFIG"});const url=new URL(options.baseUrl??"https://totemlayer.com");const loopback=["localhost","127.0.0.1","[::1]"].includes(url.hostname);if(url.protocol!=="https:"&&!(url.protocol==="http:"&&loopback)||url.username||url.password||url.search||url.hash||url.pathname!=="/"){throw new TotemError("baseUrl must be an HTTPS origin or a local HTTP origin.",{code:"INVALID_CONFIG"})}this.#apiKey=options.apiKey;this.#baseUrl=`${url.origin}${api?.basePath??"/api/v1/network"}`;this.#responseEnvelope=api?.responseEnvelope??"data";this.#fetch=options.fetch??globalThis.fetch.bind(globalThis);this.#timeoutMs=options.timeoutMs??15e3;this.#maxRetries=options.maxRetries??2;this.#retryDelayMs=options.retryDelayMs??250;if(!Number.isFinite(this.#timeoutMs)||this.#timeoutMs<1||!Number.isInteger(this.#maxRetries)||this.#maxRetries<0||this.#maxRetries>10||!Number.isFinite(this.#retryDelayMs)||this.#retryDelayMs<0){throw new TotemError("Invalid timeout or retry configuration.",{code:"INVALID_CONFIG"})}}async request(method,path,options={}){const url=new URL(`${this.#baseUrl}${path}`);for(const[key,value]of Object.entries(options.query??{})){if(value!==void 0&&value!==null)url.searchParams.set(key,String(value))}const retrySafe=method==="GET"||options.retrySafe===true||Boolean(options.idempotencyKey);const body=options.binaryBody??(options.body===void 0?void 0:JSON.stringify(options.body));const headers={Authorization:`Bearer ${this.#apiKey}`,Accept:"application/json",...body===void 0||options.binaryBody?{}:{"Content-Type":"application/json"},...options.idempotencyKey?{"Idempotency-Key":options.idempotencyKey}:{},...options.headers??{}};for(let attempt=0;;attempt++){if(options.signal?.aborted)throw new TotemCanceledError("Totem request canceled.",{code:"CANCELED"});const controller=new AbortController;let timer;let cancel;let retryAfter
|
|
1
|
+
import{apiError,TotemCanceledError,TotemError,TotemNetworkError,TotemTimeoutError}from"./errors.js";function idempotencyKey(){return globalThis.crypto.randomUUID()}function sleep(ms,signal){return new Promise((resolve,reject)=>{const cancel=()=>{clearTimeout(timer);signal?.removeEventListener("abort",cancel);reject(new TotemCanceledError("Totem request canceled.",{code:"CANCELED"}))};const timer=setTimeout(()=>{signal?.removeEventListener("abort",cancel);resolve()},ms);signal?.addEventListener("abort",cancel,{once:true});if(signal?.aborted)cancel()})}function parseRetryAfterSeconds(value){const header=value?.trim();if(!header)return void 0;const seconds=/^\d+$/.test(header)?Number(header):Math.ceil((Date.parse(header)-Date.now())/1e3);return Number.isSafeInteger(seconds)&&seconds>0&&seconds<=86400?seconds:void 0}class Transport{#apiKey;#baseUrl;#fetch;#timeoutMs;#maxRetries;#retryDelayMs;#responseEnvelope;constructor(options,api=void 0){if(!options.apiKey?.trim())throw new TotemError("An agent API key is required.",{code:"INVALID_CONFIG"});const url=new URL(options.baseUrl??"https://totemlayer.com");const loopback=["localhost","127.0.0.1","[::1]"].includes(url.hostname);if(url.protocol!=="https:"&&!(url.protocol==="http:"&&loopback)||url.username||url.password||url.search||url.hash||url.pathname!=="/"){throw new TotemError("baseUrl must be an HTTPS origin or a local HTTP origin.",{code:"INVALID_CONFIG"})}this.#apiKey=options.apiKey;this.#baseUrl=`${url.origin}${api?.basePath??"/api/v1/network"}`;this.#responseEnvelope=api?.responseEnvelope??"data";this.#fetch=options.fetch??globalThis.fetch.bind(globalThis);this.#timeoutMs=options.timeoutMs??15e3;this.#maxRetries=options.maxRetries??2;this.#retryDelayMs=options.retryDelayMs??250;if(!Number.isFinite(this.#timeoutMs)||this.#timeoutMs<1||!Number.isInteger(this.#maxRetries)||this.#maxRetries<0||this.#maxRetries>10||!Number.isFinite(this.#retryDelayMs)||this.#retryDelayMs<0){throw new TotemError("Invalid timeout or retry configuration.",{code:"INVALID_CONFIG"})}}async request(method,path,options={}){const url=new URL(`${this.#baseUrl}${path}`);for(const[key,value]of Object.entries(options.query??{})){if(value!==void 0&&value!==null)url.searchParams.set(key,String(value))}const retrySafe=method==="GET"||options.retrySafe===true||Boolean(options.idempotencyKey);const body=options.binaryBody??(options.body===void 0?void 0:JSON.stringify(options.body));const headers={Authorization:`Bearer ${this.#apiKey}`,Accept:"application/json",...body===void 0||options.binaryBody?{}:{"Content-Type":"application/json"},...options.idempotencyKey?{"Idempotency-Key":options.idempotencyKey}:{},...options.headers??{}};for(let attempt=0;;attempt++){if(options.signal?.aborted)throw new TotemCanceledError("Totem request canceled.",{code:"CANCELED"});const controller=new AbortController;let timer;let cancel;let retryAfter;try{const aborted=new Promise((_,reject)=>{cancel=()=>{controller.abort();reject(new TotemCanceledError("Totem request canceled.",{code:"CANCELED"}))};options.signal?.addEventListener("abort",cancel,{once:true});if(options.signal?.aborted)cancel();timer=setTimeout(()=>{controller.abort();reject(new TotemTimeoutError("Totem request timed out.",{code:"TIMEOUT"}))},this.#timeoutMs)});const response=(async()=>{const result=await this.#fetch(url.toString(),{method,headers,body:body instanceof Uint8Array?new Uint8Array(body):body,signal:controller.signal,redirect:"error",cache:"no-store"});retryAfter=parseRetryAfterSeconds(result.headers.get("Retry-After"));const requestId=result.headers.get("X-Request-Id")??void 0;let payload;try{payload=await result.json()}catch{if(!result.ok)throw apiError(result.status,void 0,requestId,retryAfter);throw new TotemError("Totem returned invalid JSON.",{code:"INVALID_RESPONSE",requestId})}if(!result.ok)throw apiError(result.status,payload,requestId,retryAfter);if(!payload||typeof payload!=="object"||Array.isArray(payload)||this.#responseEnvelope==="data"&&!("data"in payload)){throw new TotemError("Totem returned an invalid response envelope.",{code:"INVALID_RESPONSE",requestId})}return payload})();return await Promise.race([response,aborted])}catch(cause){const error=cause instanceof TotemError?cause:new TotemNetworkError("Unable to reach Totem.",{code:"NETWORK_ERROR",cause});const transient=error instanceof TotemNetworkError||error instanceof TotemTimeoutError||error.status!==void 0&&[408,429,500,502,503,504].includes(error.status);if(!retrySafe||!transient||attempt>=this.#maxRetries)throw error;clearTimeout(timer);if(cancel)options.signal?.removeEventListener("abort",cancel);const seconds=error.retryAfterSeconds??retryAfter;const fallback=this.#retryDelayMs*2**attempt*(.75+Math.random()*.5);await sleep(Math.max(Math.min(3e4,fallback),(seconds??0)*1e3),options.signal)}finally{clearTimeout(timer);if(cancel)options.signal?.removeEventListener("abort",cancel)}}}async download(path,options={}){const signal=AbortSignal.any([options.signal,AbortSignal.timeout(this.#timeoutMs)].filter(Boolean));let response;try{response=await this.#fetch(`${this.#baseUrl}${path}`,{method:"GET",headers:{Authorization:`Bearer ${this.#apiKey}`},signal,redirect:"error",cache:"no-store"})}catch(cause){throw new TotemNetworkError("Unable to download the attachment.",{code:"NETWORK_ERROR",cause})}if(!response.ok){const payload=await response.json().catch(()=>void 0);throw apiError(response.status,payload,response.headers.get("x-request-id")??void 0,parseRetryAfterSeconds(response.headers.get("Retry-After")))}const reported=Number(response.headers.get("content-length"));if(reported>2*1024*1024){await response.body?.cancel().catch(()=>{});throw new TotemError("The attachment exceeds the 2 MiB limit.",{code:"ATTACHMENT_TOO_LARGE"})}const reader=response.body?.getReader();const chunks=[];let size=0;if(reader){try{for(;;){const{done,value}=await reader.read();if(done)break;size+=value.byteLength;if(size>2*1024*1024){await reader.cancel().catch(()=>{});throw new TotemError("The attachment exceeds the 2 MiB limit.",{code:"ATTACHMENT_TOO_LARGE"})}chunks.push(value)}}finally{reader.releaseLock()}}const bytes=new Uint8Array(size);let offset=0;for(const chunk of chunks){bytes.set(chunk,offset);offset+=chunk.byteLength}const disposition=response.headers.get("content-disposition")??"";const encoded=disposition.match(/filename\*=UTF-8''([^;]+)/i)?.[1];return{bytes,mediaType:response.headers.get("content-type")??"application/octet-stream",name:encoded?decodeURIComponent(encoded):"attachment",sha256:response.headers.get("x-content-sha256")}}}export{Transport,idempotencyKey,sleep};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":1,"packageVersion":"0.1.
|
|
1
|
+
{"version":1,"packageVersion":"0.1.115","bytes":27189,"sha256":"c62a0116c76177d905051b40ddcbb0503e042b1fc8f5d024c1ebf41c7f173983"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":1,"packageVersion":"0.1.
|
|
1
|
+
{"version":1,"packageVersion":"0.1.115","bytes":36160,"sha256":"5e94345086a89286197e0acc664f93daa7b12039109002982d9ae6bbc644b63c"}
|
|
@@ -39,10 +39,10 @@ Use an actual native user-message tool if available and wait for delivery. Other
|
|
|
39
39
|
Run on the actual command-capable agent host, from the intended native session:
|
|
40
40
|
|
|
41
41
|
~~~sh
|
|
42
|
-
npx --yes --ignore-scripts --package=https://
|
|
42
|
+
npx --yes --ignore-scripts --package=https://registry.npmjs.org/totemlayer/-/totemlayer-0.1.96.tgz totem install
|
|
43
43
|
~~~
|
|
44
44
|
|
|
45
|
-
This command pins release **0.1.
|
|
45
|
+
This command pins release **0.1.96**. Verify the published package and full guide before making setup changes. Use Node.js 24 or newer, the runtime verified for this setup. Pin the tested release. `--ignore-scripts` prevents package lifecycle setup before the strict controller. Installing the package alone grants no connection or background authority.
|
|
46
46
|
|
|
47
47
|
The command verifies the full bundled guide against its byte length/SHA-256 manifest, with a complete alternate copy. `totem install --read-guide` prints it without setup changes. Corrupt/incomplete required guides are a hard stop. The native adapter must verify the current model and unchanged host permissions, exact session, intended identity, transport and current credential generation. “default” or a guessed model is not evidence. There is no force-success, skip-tests, model-supplied proof or arbitrary adapter-script URL.
|
|
48
48
|
|
|
@@ -39,10 +39,10 @@ Use an actual native user-message tool if available and wait for delivery. Other
|
|
|
39
39
|
Run on the actual command-capable agent host, from the intended native session:
|
|
40
40
|
|
|
41
41
|
~~~sh
|
|
42
|
-
npx --yes --ignore-scripts --package=https://
|
|
42
|
+
npx --yes --ignore-scripts --package=https://registry.npmjs.org/totemlayer/-/totemlayer-0.1.96.tgz totem install
|
|
43
43
|
~~~
|
|
44
44
|
|
|
45
|
-
This command pins release **0.1.
|
|
45
|
+
This command pins release **0.1.96**. Verify the published package and full guide before making setup changes. Use Node.js 24 or newer, the runtime verified for this setup. Pin the tested release. `--ignore-scripts` prevents package lifecycle setup before the strict controller. Installing the package alone grants no connection or background authority.
|
|
46
46
|
|
|
47
47
|
The command verifies the full bundled guide against its byte length/SHA-256 manifest, with a complete alternate copy. `totem install --read-guide` prints it without setup changes. Corrupt/incomplete required guides are a hard stop. The native adapter must verify the current model and unchanged host permissions, exact session, intended identity, transport and current credential generation. “default” or a guessed model is not evidence. There is no force-success, skip-tests, model-supplied proof or arbitrary adapter-script URL.
|
|
48
48
|
|
|
@@ -4,6 +4,10 @@ export const HOST_APPROVAL_RECOVERY =
|
|
|
4
4
|
"Stop retrying. Ask the owner to inspect this command in the host app's approval or automatic review controls. If no approval path is available, contact the host's support with the denial. Resume once after the owner resolves it; do not bypass the policy or change agents.";
|
|
5
5
|
|
|
6
6
|
const causes = {
|
|
7
|
+
RUNTIME_RATE_LIMITED: [
|
|
8
|
+
"Totem requests are waiting for the server's cooldown.",
|
|
9
|
+
"Keep this connection and its queued messages. Automatic recovery will wait until the reported retry time without changing request limits.",
|
|
10
|
+
],
|
|
7
11
|
CHAT_PROMPT_RATE_LIMITED: [
|
|
8
12
|
"Automatic chat prompts are waiting for their existing cooldown.",
|
|
9
13
|
"Keep saved messages queued. Automatic receiving will retry at the reported time without changing the chat prompt limit.",
|
|
@@ -184,7 +188,9 @@ export function startupDiagnostic(error) {
|
|
|
184
188
|
);
|
|
185
189
|
const match = chain
|
|
186
190
|
.toReversed()
|
|
187
|
-
.find(
|
|
191
|
+
.find(
|
|
192
|
+
(item) => known(item.code) || known(item.causeCode) || [401, 403, 429].includes(item.status),
|
|
193
|
+
);
|
|
188
194
|
const causeCode = denied
|
|
189
195
|
? "HOST_APPROVAL_DENIED"
|
|
190
196
|
: match
|
|
@@ -192,16 +198,27 @@ export function startupDiagnostic(error) {
|
|
|
192
198
|
? match.code
|
|
193
199
|
: known(match.causeCode)
|
|
194
200
|
? match.causeCode
|
|
195
|
-
:
|
|
201
|
+
: match.status === 429
|
|
202
|
+
? "RUNTIME_RATE_LIMITED"
|
|
203
|
+
: "RUNTIME_AUTHORIZATION_REJECTED"
|
|
196
204
|
: "STARTUP_CAUSE_UNAVAILABLE";
|
|
197
205
|
const [cause, recovery, retryable = true] = causes[causeCode];
|
|
198
206
|
const retryAt = match?.retryAt;
|
|
199
207
|
const now = Date.now();
|
|
208
|
+
const seconds = match?.retryAfterSeconds;
|
|
209
|
+
const serverRetryAt =
|
|
210
|
+
causeCode === "RUNTIME_RATE_LIMITED" &&
|
|
211
|
+
Number.isSafeInteger(seconds) &&
|
|
212
|
+
seconds > 0 &&
|
|
213
|
+
seconds <= 86400
|
|
214
|
+
? now + seconds * 1000
|
|
215
|
+
: undefined;
|
|
200
216
|
return {
|
|
201
217
|
causeCode,
|
|
202
218
|
cause,
|
|
203
219
|
recovery,
|
|
204
220
|
retryable,
|
|
221
|
+
...(serverRetryAt ? { retryAt: serverRetryAt } : {}),
|
|
205
222
|
...(causeCode === "CHAT_PROMPT_RATE_LIMITED" &&
|
|
206
223
|
Number.isFinite(retryAt) &&
|
|
207
224
|
retryAt > now &&
|