@flowcodex/core 0.3.0
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/LICENSE +21 -0
- package/README.md +9 -0
- package/dist/index-LbxYtxxS.d.ts +560 -0
- package/dist/index.d.ts +995 -0
- package/dist/index.js +3840 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/index.d.ts +1 -0
- package/dist/kernel/index.js +551 -0
- package/dist/kernel/index.js.map +1 -0
- package/package.json +39 -0
- package/src/agent/agent-loop.ts +254 -0
- package/src/agent/context.ts +99 -0
- package/src/agent/conversation-state.ts +44 -0
- package/src/agent/provider-runner.ts +241 -0
- package/src/agent/system-prompt-builder.ts +193 -0
- package/src/execution/compactor.ts +256 -0
- package/src/execution/index.ts +7 -0
- package/src/execution/output-serializer.ts +90 -0
- package/src/execution/schema-validator.ts +124 -0
- package/src/execution/tool-executor.ts +276 -0
- package/src/execution/tool-registry.ts +104 -0
- package/src/index.ts +215 -0
- package/src/infrastructure/catalog-parser.ts +218 -0
- package/src/infrastructure/index.ts +16 -0
- package/src/infrastructure/path-resolver.ts +123 -0
- package/src/infrastructure/provider-factory.ts +116 -0
- package/src/infrastructure/provider-presets.ts +19 -0
- package/src/infrastructure/retry-policy.ts +50 -0
- package/src/infrastructure/secret-scrubber.ts +67 -0
- package/src/infrastructure/token-counter.ts +156 -0
- package/src/infrastructure/tracer.ts +23 -0
- package/src/kernel/container.ts +166 -0
- package/src/kernel/events.ts +323 -0
- package/src/kernel/index.ts +18 -0
- package/src/kernel/pipeline.ts +152 -0
- package/src/kernel/run-controller.ts +85 -0
- package/src/kernel/tokens.ts +21 -0
- package/src/security/index.ts +13 -0
- package/src/security/permission-policy.ts +273 -0
- package/src/session/audit-log.ts +201 -0
- package/src/session/auth-service.ts +178 -0
- package/src/session/index.ts +26 -0
- package/src/session/secret-vault.ts +183 -0
- package/src/session/session-store.ts +339 -0
- package/src/session/types.ts +100 -0
- package/src/types/blocks.ts +56 -0
- package/src/types/context.ts +54 -0
- package/src/types/errors.ts +359 -0
- package/src/types/index.ts +34 -0
- package/src/types/provider.ts +58 -0
- package/src/types/tool.ts +39 -0
- package/src/utils/error.ts +3 -0
- package/src/utils/fs.ts +185 -0
- package/src/utils/image-resize.ts +76 -0
- package/src/utils/ssrf-guard.ts +133 -0
- package/src/utils/ulid.ts +72 -0
- package/src/utils/version-check.ts +59 -0
- package/tests/agent-loop.test.ts +490 -0
- package/tests/audit-log.test.ts +199 -0
- package/tests/auth-service.test.ts +170 -0
- package/tests/blocks.test.ts +79 -0
- package/tests/catalog-parser.test.ts +174 -0
- package/tests/compactor.test.ts +180 -0
- package/tests/container.test.ts +224 -0
- package/tests/conversation-state.test.ts +75 -0
- package/tests/errors.test.ts +429 -0
- package/tests/events-v021.test.ts +60 -0
- package/tests/events-v022.test.ts +75 -0
- package/tests/events.test.ts +340 -0
- package/tests/fixtures/large-image.png +0 -0
- package/tests/fixtures/small-image.png +0 -0
- package/tests/fs-utils.test.ts +164 -0
- package/tests/image-resize.test.ts +51 -0
- package/tests/output-serializer.test.ts +79 -0
- package/tests/path-resolver.test.ts +91 -0
- package/tests/permission-policy.test.ts +174 -0
- package/tests/pipeline.test.ts +193 -0
- package/tests/provider-factory.test.ts +245 -0
- package/tests/provider-runner.test.ts +535 -0
- package/tests/retry-policy.test.ts +104 -0
- package/tests/run-controller.test.ts +115 -0
- package/tests/sanity.test.ts +26 -0
- package/tests/schema-validator.test.ts +109 -0
- package/tests/secret-scrubber.test.ts +133 -0
- package/tests/secret-vault.test.ts +130 -0
- package/tests/session-store.test.ts +429 -0
- package/tests/ssrf-guard.test.ts +112 -0
- package/tests/system-prompt-builder.test.ts +116 -0
- package/tests/token-counter.test.ts +163 -0
- package/tests/tokens.test.ts +42 -0
- package/tests/tool-executor.test.ts +452 -0
- package/tests/tool-registry.test.ts +143 -0
- package/tests/tracer.test.ts +32 -0
- package/tests/ulid.test.ts +53 -0
- package/tests/version-check.test.ts +57 -0
- package/tsconfig.json +11 -0
- package/tsup.config.ts +16 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
|
|
4
|
+
let _photon: {
|
|
5
|
+
initSync(opts: { module: Buffer }): void;
|
|
6
|
+
PhotonImage: {
|
|
7
|
+
new_from_byteslice(data: Uint8Array): PhotonImage;
|
|
8
|
+
};
|
|
9
|
+
resize(img: PhotonImage, w: number, h: number, filter: number): PhotonImage;
|
|
10
|
+
} | undefined;
|
|
11
|
+
interface PhotonImage {
|
|
12
|
+
get_width(): number;
|
|
13
|
+
get_height(): number;
|
|
14
|
+
get_bytes_jpeg(quality: number): Uint8Array;
|
|
15
|
+
free(): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function loadPhoton() {
|
|
19
|
+
if (_photon) return;
|
|
20
|
+
try {
|
|
21
|
+
const r = createRequire(import.meta.url);
|
|
22
|
+
const raw = r('@silvia-odwyer/photon');
|
|
23
|
+
const wasmPath = r.resolve('@silvia-odwyer/photon/photon_rs_bg.wasm');
|
|
24
|
+
raw.initSync({ module: readFileSync(wasmPath) });
|
|
25
|
+
_photon = raw as NonNullable<typeof _photon>;
|
|
26
|
+
} catch {
|
|
27
|
+
// photon unavailable — resizeImageBuffer falls back to no-op
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ResizeOptions {
|
|
32
|
+
maxWidth?: number | undefined;
|
|
33
|
+
maxHeight?: number | undefined;
|
|
34
|
+
quality?: number | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ResizeResult {
|
|
38
|
+
buffer: Buffer;
|
|
39
|
+
mediaType: string;
|
|
40
|
+
resized: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const DEFAULT_MAX_WIDTH = 1568;
|
|
44
|
+
const DEFAULT_MAX_HEIGHT = 1568;
|
|
45
|
+
const DEFAULT_QUALITY = 85;
|
|
46
|
+
|
|
47
|
+
export function resizeImageBuffer(input: Buffer, opts?: ResizeOptions): ResizeResult {
|
|
48
|
+
if (!_photon) loadPhoton();
|
|
49
|
+
const photon = _photon;
|
|
50
|
+
if (!photon) return { buffer: input, mediaType: '', resized: false };
|
|
51
|
+
const maxW = opts?.maxWidth ?? DEFAULT_MAX_WIDTH;
|
|
52
|
+
const maxH = opts?.maxHeight ?? DEFAULT_MAX_HEIGHT;
|
|
53
|
+
const quality = opts?.quality ?? DEFAULT_QUALITY;
|
|
54
|
+
|
|
55
|
+
const image = photon.PhotonImage.new_from_byteslice(new Uint8Array(input));
|
|
56
|
+
const width = image.get_width();
|
|
57
|
+
const height = image.get_height();
|
|
58
|
+
|
|
59
|
+
if (width <= maxW && height <= maxH) {
|
|
60
|
+
image.free();
|
|
61
|
+
return { buffer: input, mediaType: '', resized: false };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const ratio = Math.min(maxW / width, maxH / height);
|
|
65
|
+
const newW = Math.max(1, Math.round(width * ratio));
|
|
66
|
+
const newH = Math.max(1, Math.round(height * ratio));
|
|
67
|
+
|
|
68
|
+
const resized = photon.resize(image, newW, newH, 1);
|
|
69
|
+
const outputBytes = resized.get_bytes_jpeg(quality);
|
|
70
|
+
const outputBuffer = Buffer.from(outputBytes);
|
|
71
|
+
|
|
72
|
+
image.free();
|
|
73
|
+
resized.free();
|
|
74
|
+
|
|
75
|
+
return { buffer: outputBuffer, mediaType: 'image/jpeg', resized: true };
|
|
76
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { LookupAddress } from 'node:dns';
|
|
2
|
+
import * as dns from 'node:dns/promises';
|
|
3
|
+
import { ERROR_CODES, FlowCodexError } from '../types/errors.js';
|
|
4
|
+
|
|
5
|
+
export function isPrivateIp(ip: string): boolean {
|
|
6
|
+
if (ip.includes(':')) {
|
|
7
|
+
return isPrivateIpv6(ip.toLowerCase());
|
|
8
|
+
}
|
|
9
|
+
return isPrivateIpv4(ip);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isPrivateIpv4(ip: string): boolean {
|
|
13
|
+
const parts = ip.split('.');
|
|
14
|
+
if (parts.length !== 4) return false;
|
|
15
|
+
const nums = parts.map((p) => Number.parseInt(p, 10));
|
|
16
|
+
if (nums.some((n) => Number.isNaN(n) || n < 0 || n > 255)) return false;
|
|
17
|
+
const a = nums[0];
|
|
18
|
+
const b = nums[1];
|
|
19
|
+
if (a === undefined || b === undefined) return false;
|
|
20
|
+
if (a === 10) return true;
|
|
21
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
22
|
+
if (a === 192 && b === 168) return true;
|
|
23
|
+
if (a === 127) return true;
|
|
24
|
+
if (a === 169 && b === 254) return true;
|
|
25
|
+
if (a === 0) return true;
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isPrivateIpv6(ip: string): boolean {
|
|
30
|
+
if (ip === '::1') return true;
|
|
31
|
+
if (ip.startsWith('fc') || ip.startsWith('fd')) return true;
|
|
32
|
+
if (
|
|
33
|
+
ip.startsWith('fe8') ||
|
|
34
|
+
ip.startsWith('fe9') ||
|
|
35
|
+
ip.startsWith('fea') ||
|
|
36
|
+
ip.startsWith('feb')
|
|
37
|
+
) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SafeFetchOptions extends RequestInit {
|
|
44
|
+
maxRedirects?: number | undefined;
|
|
45
|
+
maxSize?: number | undefined;
|
|
46
|
+
timeoutMs?: number | undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isAllowPrivate(): boolean {
|
|
50
|
+
return process.env.FLOWCODEX_FETCH_ALLOW_PRIVATE === '1';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function assertSafeHost(parsed: URL): Promise<void> {
|
|
54
|
+
if (isAllowPrivate()) return;
|
|
55
|
+
let addresses: LookupAddress[];
|
|
56
|
+
try {
|
|
57
|
+
addresses = await dns.lookup(parsed.hostname, { all: true });
|
|
58
|
+
} catch (err) {
|
|
59
|
+
throw new FlowCodexError({
|
|
60
|
+
message: `DNS resolution failed for ${parsed.hostname}: ${err instanceof Error ? err.message : String(err)}`,
|
|
61
|
+
code: ERROR_CODES.WEBFETCH_FAILED,
|
|
62
|
+
subsystem: 'general',
|
|
63
|
+
context: { host: parsed.hostname },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
for (const addr of addresses) {
|
|
67
|
+
if (isPrivateIp(addr.address)) {
|
|
68
|
+
throw new FlowCodexError({
|
|
69
|
+
message: `Blocked: ${parsed.hostname} resolves to private IP ${addr.address}`,
|
|
70
|
+
code: ERROR_CODES.SSRF_BLOCKED,
|
|
71
|
+
subsystem: 'general',
|
|
72
|
+
context: { url: parsed.toString(), ip: addr.address, reason: 'private' },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function safeFetch(url: string, opts: SafeFetchOptions = {}): Promise<Response> {
|
|
79
|
+
const maxRedirects = opts.maxRedirects ?? 5;
|
|
80
|
+
const timeoutMs = opts.timeoutMs ?? 15_000;
|
|
81
|
+
|
|
82
|
+
const fetchOpts: RequestInit = { ...opts };
|
|
83
|
+
delete (fetchOpts as Partial<SafeFetchOptions>).maxRedirects;
|
|
84
|
+
delete (fetchOpts as Partial<SafeFetchOptions>).maxSize;
|
|
85
|
+
delete (fetchOpts as Partial<SafeFetchOptions>).timeoutMs;
|
|
86
|
+
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
89
|
+
if (opts.signal) {
|
|
90
|
+
opts.signal.addEventListener('abort', () => controller.abort());
|
|
91
|
+
}
|
|
92
|
+
fetchOpts.signal = controller.signal;
|
|
93
|
+
|
|
94
|
+
let currentUrl = url;
|
|
95
|
+
for (let i = 0; i <= maxRedirects; i++) {
|
|
96
|
+
const parsed = new URL(currentUrl);
|
|
97
|
+
await assertSafeHost(parsed);
|
|
98
|
+
|
|
99
|
+
let response: Response;
|
|
100
|
+
try {
|
|
101
|
+
response = await fetch(currentUrl, { ...fetchOpts, redirect: 'manual' });
|
|
102
|
+
} catch (err) {
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
throw new FlowCodexError({
|
|
105
|
+
message: `Fetch failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
106
|
+
code: ERROR_CODES.WEBFETCH_FAILED,
|
|
107
|
+
subsystem: 'general',
|
|
108
|
+
context: { url: currentUrl },
|
|
109
|
+
cause: err,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (response.status >= 300 && response.status < 400) {
|
|
114
|
+
const location = response.headers.get('location');
|
|
115
|
+
if (!location) {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
return response;
|
|
118
|
+
}
|
|
119
|
+
currentUrl = new URL(location, currentUrl).toString();
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
return response;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
throw new FlowCodexError({
|
|
129
|
+
message: `Too many redirects (max ${maxRedirects})`,
|
|
130
|
+
code: ERROR_CODES.WEBFETCH_FAILED,
|
|
131
|
+
subsystem: 'general',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
2
|
+
const ENCODING_LEN = 32;
|
|
3
|
+
const TIME_LEN = 10;
|
|
4
|
+
const RANDOM_LEN = 16;
|
|
5
|
+
|
|
6
|
+
import { randomFillSync } from 'node:crypto';
|
|
7
|
+
|
|
8
|
+
let lastTime = 0;
|
|
9
|
+
let lastRandom: Uint8Array = new Uint8Array(16);
|
|
10
|
+
|
|
11
|
+
function encodeTime(now: number, len: number): string {
|
|
12
|
+
let str = '';
|
|
13
|
+
for (let i = len - 1; i >= 0; i--) {
|
|
14
|
+
const mod = now % ENCODING_LEN;
|
|
15
|
+
str = (ENCODING[mod] ?? '0') + str;
|
|
16
|
+
now = (now - mod) / ENCODING_LEN;
|
|
17
|
+
}
|
|
18
|
+
return str;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function encodeRandom(bytes: Uint8Array, len: number): string {
|
|
22
|
+
let str = '';
|
|
23
|
+
for (let i = 0; i < len; i++) {
|
|
24
|
+
const byte = bytes[i] ?? 0;
|
|
25
|
+
str += ENCODING[byte % ENCODING_LEN] ?? '0';
|
|
26
|
+
}
|
|
27
|
+
return str;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function ulid(now: number = Date.now()): string {
|
|
31
|
+
if (now <= lastTime) {
|
|
32
|
+
for (let i = 15; i >= 0; i--) {
|
|
33
|
+
const val = lastRandom[i] ?? 0;
|
|
34
|
+
if (val === 0xff) {
|
|
35
|
+
lastRandom[i] = 0;
|
|
36
|
+
} else {
|
|
37
|
+
lastRandom[i] = val + 1;
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
const buf = new Uint8Array(16);
|
|
43
|
+
randomFillSync(buf);
|
|
44
|
+
lastRandom = buf;
|
|
45
|
+
}
|
|
46
|
+
lastTime = now;
|
|
47
|
+
return encodeTime(now, TIME_LEN) + encodeRandom(lastRandom, RANDOM_LEN);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function ulidTime(ulidStr: string): number {
|
|
51
|
+
if (ulidStr.length !== TIME_LEN + RANDOM_LEN) {
|
|
52
|
+
throw new Error(`Invalid ULID length: expected ${TIME_LEN + RANDOM_LEN}, got ${ulidStr.length}`);
|
|
53
|
+
}
|
|
54
|
+
const timeChars = ulidStr.slice(0, TIME_LEN);
|
|
55
|
+
let time = 0;
|
|
56
|
+
for (let i = 0; i < TIME_LEN; i++) {
|
|
57
|
+
const ch = timeChars[i] ?? '';
|
|
58
|
+
const idx = ENCODING.indexOf(ch);
|
|
59
|
+
if (idx === -1) throw new Error(`Invalid ULID character: ${ch}`);
|
|
60
|
+
time = time * ENCODING_LEN + idx;
|
|
61
|
+
}
|
|
62
|
+
return time;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isUlid(str: string): boolean {
|
|
66
|
+
if (str.length !== TIME_LEN + RANDOM_LEN) return false;
|
|
67
|
+
for (let i = 0; i < str.length; i++) {
|
|
68
|
+
const ch = str[i] ?? '';
|
|
69
|
+
if (ENCODING.indexOf(ch) === -1) return false;
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface VersionInfo {
|
|
2
|
+
current: string;
|
|
3
|
+
latest: string;
|
|
4
|
+
updateAvailable: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const REGISTRY_URL = 'https://registry.npmjs.org';
|
|
8
|
+
const CHECK_TIMEOUT_MS = 3_000;
|
|
9
|
+
|
|
10
|
+
export async function fetchLatestVersion(
|
|
11
|
+
packageName: string,
|
|
12
|
+
opts?: { fetchImpl?: typeof fetch; registryUrl?: string; timeoutMs?: number },
|
|
13
|
+
): Promise<string | undefined> {
|
|
14
|
+
const fetchImpl = opts?.fetchImpl ?? fetch;
|
|
15
|
+
const base = opts?.registryUrl ?? REGISTRY_URL;
|
|
16
|
+
const timeout = opts?.timeoutMs ?? CHECK_TIMEOUT_MS;
|
|
17
|
+
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetchImpl(`${base}/${packageName}/latest`, {
|
|
22
|
+
signal: controller.signal,
|
|
23
|
+
});
|
|
24
|
+
if (!res.ok) return undefined;
|
|
25
|
+
const body = (await res.json()) as { version?: string };
|
|
26
|
+
return body.version;
|
|
27
|
+
} catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
} finally {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function compareVersions(current: string, latest: string): -1 | 0 | 1 {
|
|
35
|
+
const parse = (v: string): number[] => v.split('.').map(Number);
|
|
36
|
+
const a = parse(current);
|
|
37
|
+
const b = parse(latest);
|
|
38
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
39
|
+
const av = a[i] ?? 0;
|
|
40
|
+
const bv = b[i] ?? 0;
|
|
41
|
+
if (av < bv) return -1;
|
|
42
|
+
if (av > bv) return 1;
|
|
43
|
+
}
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function checkForUpdate(
|
|
48
|
+
currentVersion: string,
|
|
49
|
+
packageName: string,
|
|
50
|
+
opts?: { fetchImpl?: typeof fetch; registryUrl?: string },
|
|
51
|
+
): Promise<VersionInfo | undefined> {
|
|
52
|
+
const latest = await fetchLatestVersion(packageName, opts);
|
|
53
|
+
if (!latest) return undefined;
|
|
54
|
+
return {
|
|
55
|
+
current: currentVersion,
|
|
56
|
+
latest,
|
|
57
|
+
updateAvailable: compareVersions(currentVersion, latest) < 0,
|
|
58
|
+
};
|
|
59
|
+
}
|