@amaster.ai/employee-runtime-connector 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/amaster-runtime-daemon.mjs +809 -520
- package/dist/amaster-runtime.mjs +1 -1
- package/dist/pi-exact-source-read.mjs +213 -0
- package/dist/public-network-scope.mjs +120 -0
- package/package.json +1 -1
package/dist/amaster-runtime.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { homedir, hostname } from "node:os";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { BUILD_SUPPORTED_CAPABILITIES } from "./amaster-runtime-daemon/capability-registry.mjs";
|
|
9
9
|
|
|
10
|
-
const CONNECTOR_VERSION = "0.1.
|
|
10
|
+
const CONNECTOR_VERSION = "0.1.7";
|
|
11
11
|
|
|
12
12
|
const CONFIG_KEY_MAP = new Map([
|
|
13
13
|
["server_url", "AMASTER_EMPLOYEE_SERVER_URL"],
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { chmodSync, mkdirSync, renameSync, rmSync, writeFileSync, writeSync } from "node:fs";
|
|
3
|
+
import http from "node:http";
|
|
4
|
+
import https from "node:https";
|
|
5
|
+
import { isIP } from "node:net";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { createPublicNetworkScope } from "./public-network-scope.mjs";
|
|
8
|
+
|
|
9
|
+
export const EXACT_SOURCE_READ_TOOL_NAME = "amaster_exact_source_read";
|
|
10
|
+
const MAX_REDIRECTS = 5;
|
|
11
|
+
const MAX_BYTES = 512 * 1024;
|
|
12
|
+
const ALLOWED_MEDIA_TYPES = new Set([
|
|
13
|
+
"application/json",
|
|
14
|
+
"application/xml",
|
|
15
|
+
"application/xhtml+xml",
|
|
16
|
+
"text/html",
|
|
17
|
+
"text/plain",
|
|
18
|
+
"text/xml",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
function exactSourceError(code) {
|
|
22
|
+
return Object.assign(new Error(code), { code });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function exactSourceToolAllowlist(profile) {
|
|
26
|
+
const governedToolNames = Array.isArray(profile?.directToolNames)
|
|
27
|
+
? profile.directToolNames.filter((name) => typeof name === "string" && name)
|
|
28
|
+
: [];
|
|
29
|
+
return [...new Set([...governedToolNames, EXACT_SOURCE_READ_TOOL_NAME])];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeText(body, mediaType) {
|
|
33
|
+
let value = body.toString("utf8");
|
|
34
|
+
if (mediaType === "text/html" || mediaType === "application/xhtml+xml") {
|
|
35
|
+
value = value
|
|
36
|
+
.replace(/<(?:script|style|noscript|template)\b[^>]*>[\s\S]*?<\/(?:script|style|noscript|template)>/giu, " ")
|
|
37
|
+
.replace(/<[^>]+>/gu, " ")
|
|
38
|
+
.replace(/ /giu, " ")
|
|
39
|
+
.replace(/&/giu, "&")
|
|
40
|
+
.replace(/</giu, "<")
|
|
41
|
+
.replace(/>/giu, ">");
|
|
42
|
+
}
|
|
43
|
+
return value
|
|
44
|
+
.replace(/\r\n?/gu, "\n")
|
|
45
|
+
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/gu, "")
|
|
46
|
+
.replace(/[ \t]+/gu, " ")
|
|
47
|
+
.replace(/\n{3,}/gu, "\n\n")
|
|
48
|
+
.trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function defaultRequestOnce({ url, addresses, signal }) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const transport = url.protocol === "https:" ? https : http;
|
|
54
|
+
const selectedAddress = addresses[0];
|
|
55
|
+
const family = isIP(selectedAddress);
|
|
56
|
+
const request = transport.request(url, {
|
|
57
|
+
method: "GET",
|
|
58
|
+
headers: {
|
|
59
|
+
accept: "text/html, text/plain, application/json, application/xml;q=0.9",
|
|
60
|
+
"accept-encoding": "identity",
|
|
61
|
+
"user-agent": "MirrorX-Exact-Source-Reader/1.0",
|
|
62
|
+
},
|
|
63
|
+
lookup(_hostname, options, callback) {
|
|
64
|
+
if (options?.all === true) {
|
|
65
|
+
callback(null, [{ address: selectedAddress, family }]);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
callback(null, selectedAddress, family);
|
|
69
|
+
},
|
|
70
|
+
signal,
|
|
71
|
+
}, (response) => {
|
|
72
|
+
const declaredLength = Number(response.headers["content-length"] ?? 0);
|
|
73
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_BYTES) {
|
|
74
|
+
request.destroy(exactSourceError("source_reader_oversize"));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const chunks = [];
|
|
78
|
+
let bytes = 0;
|
|
79
|
+
response.on("data", (chunk) => {
|
|
80
|
+
bytes += chunk.length;
|
|
81
|
+
if (bytes > MAX_BYTES) {
|
|
82
|
+
request.destroy(exactSourceError("source_reader_oversize"));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
chunks.push(chunk);
|
|
86
|
+
});
|
|
87
|
+
response.on("end", () => resolve({
|
|
88
|
+
statusCode: response.statusCode ?? 0,
|
|
89
|
+
headers: response.headers,
|
|
90
|
+
body: Buffer.concat(chunks),
|
|
91
|
+
}));
|
|
92
|
+
});
|
|
93
|
+
request.setTimeout(15_000, () => request.destroy(exactSourceError("source_reader_timeout")));
|
|
94
|
+
request.on("error", reject);
|
|
95
|
+
request.end();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function readExactPublicSource(locator, options = {}) {
|
|
100
|
+
const initial = new URL(locator);
|
|
101
|
+
const expectedOrigin = initial.origin;
|
|
102
|
+
const networkScope = createPublicNetworkScope(options.lookup ? { lookup: options.lookup } : {});
|
|
103
|
+
const requestOnce = options.requestOnce ?? defaultRequestOnce;
|
|
104
|
+
let current = initial;
|
|
105
|
+
for (let redirectCount = 0; redirectCount <= MAX_REDIRECTS; redirectCount += 1) {
|
|
106
|
+
if (current.origin !== expectedOrigin) throw exactSourceError("source_reader_redirect_scope_forbidden");
|
|
107
|
+
const addresses = await networkScope.resolveNetworkAddresses(current.toString());
|
|
108
|
+
const response = await requestOnce({
|
|
109
|
+
url: current,
|
|
110
|
+
addresses,
|
|
111
|
+
signal: options.signal,
|
|
112
|
+
});
|
|
113
|
+
if ([301, 302, 303, 307, 308].includes(response.statusCode)) {
|
|
114
|
+
const location = Array.isArray(response.headers.location)
|
|
115
|
+
? response.headers.location[0]
|
|
116
|
+
: response.headers.location;
|
|
117
|
+
if (!location || redirectCount === MAX_REDIRECTS) {
|
|
118
|
+
throw exactSourceError("source_reader_redirect_invalid");
|
|
119
|
+
}
|
|
120
|
+
current = new URL(location, current);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (response.statusCode === 401 || response.statusCode === 403) {
|
|
124
|
+
throw exactSourceError("user_link_auth_required");
|
|
125
|
+
}
|
|
126
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
127
|
+
throw exactSourceError("source_unavailable");
|
|
128
|
+
}
|
|
129
|
+
const contentEncoding = String(response.headers["content-encoding"] ?? "identity").toLowerCase();
|
|
130
|
+
if (contentEncoding !== "identity") throw exactSourceError("source_reader_content_encoding_forbidden");
|
|
131
|
+
const mediaType = String(response.headers["content-type"] ?? "text/plain")
|
|
132
|
+
.split(";", 1)[0].trim().toLowerCase();
|
|
133
|
+
if (!ALLOWED_MEDIA_TYPES.has(mediaType)) throw exactSourceError("source_reader_content_type_forbidden");
|
|
134
|
+
const normalizedText = normalizeText(response.body, mediaType);
|
|
135
|
+
if (!normalizedText) throw exactSourceError("source_reader_empty");
|
|
136
|
+
return {
|
|
137
|
+
requestedUrl: initial.toString(),
|
|
138
|
+
finalUrl: current.toString(),
|
|
139
|
+
mediaType,
|
|
140
|
+
normalizedText,
|
|
141
|
+
truncated: false,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
throw exactSourceError("source_reader_redirect_invalid");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function registerExactSourceReadTool(pi, binding, options = {}) {
|
|
148
|
+
pi.registerTool({
|
|
149
|
+
name: EXACT_SOURCE_READ_TOOL_NAME,
|
|
150
|
+
label: "Read the frozen user source",
|
|
151
|
+
description: "Read the one exact user-provided URL frozen by the Server. This tool accepts no URL argument.",
|
|
152
|
+
parameters: { type: "object", additionalProperties: false, properties: {} },
|
|
153
|
+
executionMode: "sequential",
|
|
154
|
+
execute: async (toolCallId, _params, signal) => {
|
|
155
|
+
const evidence = await readExactPublicSource(binding.locator, { ...options, signal });
|
|
156
|
+
const capturedAt = new Date().toISOString();
|
|
157
|
+
writeSync(1, `${JSON.stringify({
|
|
158
|
+
type: "amaster_web_evidence",
|
|
159
|
+
version: "runtime_web_evidence_v1",
|
|
160
|
+
evidence: {
|
|
161
|
+
toolCallId,
|
|
162
|
+
requestedUrl: evidence.requestedUrl,
|
|
163
|
+
finalUrl: evidence.finalUrl,
|
|
164
|
+
capturedAt,
|
|
165
|
+
mediaType: evidence.mediaType,
|
|
166
|
+
normalizedText: evidence.normalizedText,
|
|
167
|
+
truncated: evidence.truncated,
|
|
168
|
+
},
|
|
169
|
+
})}\n`);
|
|
170
|
+
return {
|
|
171
|
+
content: [{ type: "text", text: evidence.normalizedText }],
|
|
172
|
+
details: {
|
|
173
|
+
requestedUrl: evidence.requestedUrl,
|
|
174
|
+
finalUrl: evidence.finalUrl,
|
|
175
|
+
capturedAt,
|
|
176
|
+
mediaType: evidence.mediaType,
|
|
177
|
+
contentSha256: createHash("sha256").update(evidence.normalizedText).digest("hex"),
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function managedPiExactSourceReadExtensionSource(binding, options = {}) {
|
|
185
|
+
const runtimeModuleUrl = typeof options.runtimeModuleUrl === "string" && options.runtimeModuleUrl
|
|
186
|
+
? options.runtimeModuleUrl
|
|
187
|
+
: import.meta.url;
|
|
188
|
+
// Source tests resolve back to this module. The bundled daemon resolves to
|
|
189
|
+
// the side-effect-free dist sidecar copied by scripts/build.mjs. Never import
|
|
190
|
+
// the daemon entry itself from a Pi extension: doing so executes its CLI main.
|
|
191
|
+
const readerModuleUrl = new URL("./pi-exact-source-read.mjs", runtimeModuleUrl).href;
|
|
192
|
+
return `import { registerExactSourceReadTool } from ${JSON.stringify(readerModuleUrl)};\n`
|
|
193
|
+
+ `const binding = ${JSON.stringify(binding)};\n`
|
|
194
|
+
+ "export default function amasterExactSourceRead(pi) { registerExactSourceReadTool(pi, binding); }\n";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function materializePiExactSourceReadExtension(profileRoot, binding) {
|
|
198
|
+
const extensionPath = join(profileRoot, "extensions", "amaster-exact-source-read.js");
|
|
199
|
+
mkdirSync(dirname(extensionPath), { recursive: true, mode: 0o700 });
|
|
200
|
+
const temporaryPath = `${extensionPath}.${process.pid}.tmp`;
|
|
201
|
+
try {
|
|
202
|
+
writeFileSync(temporaryPath, managedPiExactSourceReadExtensionSource(binding), {
|
|
203
|
+
encoding: "utf8",
|
|
204
|
+
mode: 0o600,
|
|
205
|
+
flag: "wx",
|
|
206
|
+
});
|
|
207
|
+
renameSync(temporaryPath, extensionPath);
|
|
208
|
+
chmodSync(extensionPath, 0o600);
|
|
209
|
+
return extensionPath;
|
|
210
|
+
} finally {
|
|
211
|
+
rmSync(temporaryPath, { force: true });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { lookup as dnsLookup } from "node:dns/promises";
|
|
2
|
+
import { BlockList, isIP } from "node:net";
|
|
3
|
+
|
|
4
|
+
const blockedV4Addresses = new BlockList();
|
|
5
|
+
for (const [network, prefix] of [
|
|
6
|
+
["0.0.0.0", 8],
|
|
7
|
+
["10.0.0.0", 8],
|
|
8
|
+
["100.64.0.0", 10],
|
|
9
|
+
["127.0.0.0", 8],
|
|
10
|
+
["169.254.0.0", 16],
|
|
11
|
+
["172.16.0.0", 12],
|
|
12
|
+
["192.0.0.0", 24],
|
|
13
|
+
["192.0.2.0", 24],
|
|
14
|
+
["192.88.99.0", 24],
|
|
15
|
+
["192.168.0.0", 16],
|
|
16
|
+
["198.18.0.0", 15],
|
|
17
|
+
["198.51.100.0", 24],
|
|
18
|
+
["203.0.113.0", 24],
|
|
19
|
+
["224.0.0.0", 4],
|
|
20
|
+
["240.0.0.0", 4],
|
|
21
|
+
]) {
|
|
22
|
+
blockedV4Addresses.addSubnet(network, prefix, "ipv4");
|
|
23
|
+
}
|
|
24
|
+
const blockedV6Addresses = new BlockList();
|
|
25
|
+
for (const [network, prefix] of [
|
|
26
|
+
["::", 128],
|
|
27
|
+
["::1", 128],
|
|
28
|
+
["::", 96],
|
|
29
|
+
["::ffff:0:0", 96],
|
|
30
|
+
["64:ff9b::", 96],
|
|
31
|
+
["64:ff9b:1::", 48],
|
|
32
|
+
["100::", 64],
|
|
33
|
+
["2001::", 32],
|
|
34
|
+
["2001:2::", 48],
|
|
35
|
+
["2001:db8::", 32],
|
|
36
|
+
["2001:10::", 28],
|
|
37
|
+
["2001:20::", 28],
|
|
38
|
+
["2002::", 16],
|
|
39
|
+
["fc00::", 7],
|
|
40
|
+
["fe80::", 10],
|
|
41
|
+
["fec0::", 10],
|
|
42
|
+
["ff00::", 8],
|
|
43
|
+
]) {
|
|
44
|
+
blockedV6Addresses.addSubnet(network, prefix, "ipv6");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function forbidden() {
|
|
48
|
+
return Object.assign(new Error("source_reader_network_scope_forbidden"), {
|
|
49
|
+
code: "source_reader_network_scope_forbidden",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parsePublicLocator(value) {
|
|
54
|
+
let url;
|
|
55
|
+
try {
|
|
56
|
+
url = new URL(value);
|
|
57
|
+
} catch {
|
|
58
|
+
throw forbidden();
|
|
59
|
+
}
|
|
60
|
+
if (
|
|
61
|
+
(url.protocol !== "https:" && url.protocol !== "http:")
|
|
62
|
+
|| url.username
|
|
63
|
+
|| url.password
|
|
64
|
+
|| !url.hostname
|
|
65
|
+
) {
|
|
66
|
+
throw forbidden();
|
|
67
|
+
}
|
|
68
|
+
const hostname = url.hostname.toLowerCase().replace(/\.$/u, "");
|
|
69
|
+
if (
|
|
70
|
+
hostname === "localhost"
|
|
71
|
+
|| hostname.endsWith(".localhost")
|
|
72
|
+
|| hostname.endsWith(".local")
|
|
73
|
+
) {
|
|
74
|
+
throw forbidden();
|
|
75
|
+
}
|
|
76
|
+
return { url, hostname };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function addressIsPublic(address) {
|
|
80
|
+
const family = isIP(address);
|
|
81
|
+
if (family === 4) return !blockedV4Addresses.check(address, "ipv4");
|
|
82
|
+
if (family === 6) return !blockedV6Addresses.check(address, "ipv6");
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createPublicNetworkScope(options = {}) {
|
|
87
|
+
const lookup = options.lookup ?? ((hostname) => dnsLookup(hostname, {
|
|
88
|
+
all: true,
|
|
89
|
+
verbatim: true,
|
|
90
|
+
}));
|
|
91
|
+
async function resolveNetworkAddresses(locator) {
|
|
92
|
+
const { hostname } = parsePublicLocator(locator);
|
|
93
|
+
let addresses;
|
|
94
|
+
try {
|
|
95
|
+
addresses = await lookup(hostname);
|
|
96
|
+
} catch {
|
|
97
|
+
throw forbidden();
|
|
98
|
+
}
|
|
99
|
+
if (
|
|
100
|
+
!Array.isArray(addresses)
|
|
101
|
+
|| addresses.length === 0
|
|
102
|
+
|| addresses.some((entry) => (
|
|
103
|
+
!entry
|
|
104
|
+
|| typeof entry.address !== "string"
|
|
105
|
+
|| !addressIsPublic(entry.address)
|
|
106
|
+
))
|
|
107
|
+
) {
|
|
108
|
+
throw forbidden();
|
|
109
|
+
}
|
|
110
|
+
return [...new Set(addresses.map((entry) => entry.address))].sort();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return Object.freeze({
|
|
114
|
+
resolveNetworkAddresses,
|
|
115
|
+
async assertNetworkAddressAllowed(locator) {
|
|
116
|
+
await resolveNetworkAddresses(locator);
|
|
117
|
+
return true;
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|