@lythos/cold-pool 0.13.2 → 0.14.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/package.json +2 -2
- package/src/cold-pool.ts +1 -2
- package/src/mirror.test.ts +90 -0
- package/src/mirror.ts +39 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lythos/cold-pool",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Cold pool service layer — dedicated resource holder for skill repositories with intent/plan/execute primitives. Single owner of git side-effects; consumed by deck/curator/arena.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agent",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"homepage": "https://github.com/lythos-labs/lythoskill/tree/main/packages/lythoskill-cold-pool#readme",
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@lythos/infra": "^0.
|
|
45
|
+
"@lythos/infra": "^0.14.0",
|
|
46
46
|
"simple-git": "^3.36.0"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
package/src/cold-pool.ts
CHANGED
package/src/mirror.test.ts
CHANGED
|
@@ -4,12 +4,15 @@ import { probeConnectivity } from './mirror.js'
|
|
|
4
4
|
describe('probeConnectivity', () => {
|
|
5
5
|
let originalFetch: typeof fetch
|
|
6
6
|
let originalEnv: string | undefined
|
|
7
|
+
let originalSocks: string | undefined
|
|
7
8
|
let fetchCalls: Array<{ url: string; options: RequestInit }>
|
|
8
9
|
|
|
9
10
|
beforeEach(() => {
|
|
10
11
|
originalFetch = globalThis.fetch
|
|
11
12
|
originalEnv = process.env.LYTHOSKILL_GH_MIRROR
|
|
13
|
+
originalSocks = process.env.LYTHOS_SOCKS_PROXY
|
|
12
14
|
delete process.env.LYTHOSKILL_GH_MIRROR
|
|
15
|
+
delete process.env.LYTHOS_SOCKS_PROXY
|
|
13
16
|
fetchCalls = []
|
|
14
17
|
})
|
|
15
18
|
|
|
@@ -20,6 +23,11 @@ describe('probeConnectivity', () => {
|
|
|
20
23
|
} else {
|
|
21
24
|
delete process.env.LYTHOSKILL_GH_MIRROR
|
|
22
25
|
}
|
|
26
|
+
if (originalSocks !== undefined) {
|
|
27
|
+
process.env.LYTHOS_SOCKS_PROXY = originalSocks
|
|
28
|
+
} else {
|
|
29
|
+
delete process.env.LYTHOS_SOCKS_PROXY
|
|
30
|
+
}
|
|
23
31
|
})
|
|
24
32
|
|
|
25
33
|
function mockFetch(
|
|
@@ -157,4 +165,86 @@ describe('probeConnectivity', () => {
|
|
|
157
165
|
expect(result).toBeUndefined()
|
|
158
166
|
expect(elapsed).toBeLessThan(500) // 100ms timeout + overhead
|
|
159
167
|
})
|
|
168
|
+
|
|
169
|
+
// ── Vertical Slice 8: SOCKS proxy routing ──
|
|
170
|
+
test('SOCKS proxy set, curl succeeds → returns direct path', async () => {
|
|
171
|
+
process.env.LYTHOS_SOCKS_PROXY = '127.0.0.1:1080'
|
|
172
|
+
const execCalls: Array<{ file: string; args: string[] }> = []
|
|
173
|
+
|
|
174
|
+
const mockExec = (file: unknown, args: unknown) => {
|
|
175
|
+
execCalls.push({ file: String(file), args: args as string[] })
|
|
176
|
+
return ''
|
|
177
|
+
}
|
|
178
|
+
const result = await probeConnectivity('https://example.com/skill', 3000, {
|
|
179
|
+
execFileSync: mockExec as any,
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
expect(result).toMatchObject({
|
|
183
|
+
path: 'direct',
|
|
184
|
+
url: 'https://example.com/skill',
|
|
185
|
+
latencyMs: expect.any(Number),
|
|
186
|
+
})
|
|
187
|
+
expect(execCalls.length).toBe(1)
|
|
188
|
+
expect(execCalls[0].file).toBe('curl')
|
|
189
|
+
expect(execCalls[0].args).toContain('--proxy')
|
|
190
|
+
expect(execCalls[0].args).toContain('socks5://127.0.0.1:1080')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// ── Vertical Slice 9: SOCKS proxy with socks5:// prefix ──
|
|
194
|
+
test('SOCKS proxy already has socks5:// prefix → does not double-prefix', async () => {
|
|
195
|
+
process.env.LYTHOS_SOCKS_PROXY = 'socks5://proxy.example.com:1080'
|
|
196
|
+
const execCalls: Array<{ file: string; args: string[] }> = []
|
|
197
|
+
|
|
198
|
+
const mockExec2 = (file: unknown, args: unknown) => {
|
|
199
|
+
execCalls.push({ file: String(file), args: args as string[] })
|
|
200
|
+
return ''
|
|
201
|
+
}
|
|
202
|
+
await probeConnectivity('https://example.com/skill', 3000, {
|
|
203
|
+
execFileSync: mockExec2 as any,
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
expect(execCalls[0].args).toContain('socks5://proxy.example.com:1080')
|
|
207
|
+
expect(execCalls[0].args).not.toContain('socks5://socks5://proxy.example.com:1080')
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
// ── Vertical Slice 10: SOCKS proxy fails, direct via fetch succeeds ──
|
|
211
|
+
test('SOCKS proxy fails, fetch fallback succeeds', async () => {
|
|
212
|
+
process.env.LYTHOS_SOCKS_PROXY = '127.0.0.1:1080'
|
|
213
|
+
|
|
214
|
+
const result = await probeConnectivity('https://example.com/skill', 3000, {
|
|
215
|
+
execFileSync: () => {
|
|
216
|
+
throw new Error('curl failed')
|
|
217
|
+
},
|
|
218
|
+
fetch: async () => new Response(null, { status: 200 }),
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// When SOCKS proxy is configured but curl fails, the direct probe fails.
|
|
222
|
+
// No automatic fallback to unproxied fetch — user explicitly chose proxy.
|
|
223
|
+
expect(result).toBeUndefined()
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
// ── Vertical Slice 11: SOCKS proxy only affects direct, mirror still works ──
|
|
227
|
+
test('SOCKS proxy fails but mirror succeeds', async () => {
|
|
228
|
+
process.env.LYTHOS_SOCKS_PROXY = '127.0.0.1:1080'
|
|
229
|
+
process.env.LYTHOSKILL_GH_MIRROR = 'https://my-mirror.example.com'
|
|
230
|
+
const execCalls: Array<{ file: string; args: string[] }> = []
|
|
231
|
+
|
|
232
|
+
const result = await probeConnectivity('https://example.com/skill', 3000, {
|
|
233
|
+
execFileSync: (file, args) => {
|
|
234
|
+
execCalls.push({ file: String(file), args: args as string[] })
|
|
235
|
+
throw new Error('curl failed')
|
|
236
|
+
},
|
|
237
|
+
fetch: async (input) => {
|
|
238
|
+
const url = String(input)
|
|
239
|
+
if (url.includes('my-mirror')) {
|
|
240
|
+
return new Response(null, { status: 200 })
|
|
241
|
+
}
|
|
242
|
+
throw new Error('ENOTFOUND')
|
|
243
|
+
},
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
// SOCKS proxy is only used for direct probes; mirror probes use native fetch
|
|
247
|
+
expect(result?.path).toBe('mirror')
|
|
248
|
+
expect(execCalls.length).toBe(1) // only one curl call for direct probe
|
|
249
|
+
})
|
|
160
250
|
})
|
package/src/mirror.ts
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* that can return tampered skill files (see ADR-202605130...).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { execFileSync } from 'node:child_process'
|
|
14
|
+
|
|
13
15
|
export function getMirror(): string | undefined {
|
|
14
16
|
const v = process.env.LYTHOSKILL_GH_MIRROR?.trim()
|
|
15
17
|
if (!v) return undefined
|
|
@@ -47,19 +49,30 @@ export interface ProbeFailure {
|
|
|
47
49
|
reason: string
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
export interface ProbeDeps {
|
|
53
|
+
fetch?: typeof globalThis.fetch
|
|
54
|
+
execFileSync?: typeof execFileSync
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
/**
|
|
51
58
|
* Quick connectivity probe: race direct + user mirror (if set) concurrently.
|
|
52
59
|
* Uses short timeout (default 3s) to fail fast instead of waiting for
|
|
53
60
|
* the full git clone / fetch timeout.
|
|
54
61
|
*
|
|
62
|
+
* When LYTHOS_SOCKS_PROXY is set, direct probes route through the SOCKS
|
|
63
|
+
* proxy via curl (Bun fetch does not support socks5://).
|
|
64
|
+
*
|
|
55
65
|
* Returns the first success, or undefined with failures recorded.
|
|
56
66
|
*/
|
|
57
67
|
export async function probeConnectivity(
|
|
58
68
|
url: string,
|
|
59
69
|
timeoutMs = 3000,
|
|
70
|
+
deps?: ProbeDeps,
|
|
60
71
|
): Promise<ProbeResult & { failures?: ProbeFailure[] } | undefined> {
|
|
61
72
|
const start = performance.now()
|
|
62
73
|
const failures: ProbeFailure[] = []
|
|
74
|
+
const _fetch = deps?.fetch ?? globalThis.fetch
|
|
75
|
+
const _exec = deps?.execFileSync ?? execFileSync
|
|
63
76
|
|
|
64
77
|
async function tryFetch(
|
|
65
78
|
targetUrl: string,
|
|
@@ -67,7 +80,32 @@ export async function probeConnectivity(
|
|
|
67
80
|
): Promise<ProbeResult | undefined> {
|
|
68
81
|
const t0 = performance.now()
|
|
69
82
|
try {
|
|
70
|
-
|
|
83
|
+
// Route direct probes through SOCKS proxy when configured
|
|
84
|
+
if (pathLabel === 'direct') {
|
|
85
|
+
const socksProxy = process.env.LYTHOS_SOCKS_PROXY?.trim()
|
|
86
|
+
if (socksProxy) {
|
|
87
|
+
const proxyUrl = socksProxy.startsWith('socks5://') ? socksProxy : `socks5://${socksProxy}`
|
|
88
|
+
_exec(
|
|
89
|
+
'curl',
|
|
90
|
+
[
|
|
91
|
+
'--silent',
|
|
92
|
+
'--head',
|
|
93
|
+
'--location',
|
|
94
|
+
'--proxy',
|
|
95
|
+
proxyUrl,
|
|
96
|
+
'--connect-timeout',
|
|
97
|
+
String(Math.ceil(timeoutMs / 1000)),
|
|
98
|
+
'--max-time',
|
|
99
|
+
String(Math.ceil(timeoutMs / 1000)),
|
|
100
|
+
targetUrl,
|
|
101
|
+
],
|
|
102
|
+
{ encoding: 'utf-8', timeout: timeoutMs + 500 },
|
|
103
|
+
)
|
|
104
|
+
return { path: pathLabel, url: targetUrl, latencyMs: Math.round(performance.now() - t0) }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const res = await _fetch(targetUrl, {
|
|
71
109
|
method: 'HEAD',
|
|
72
110
|
signal: AbortSignal.timeout(timeoutMs),
|
|
73
111
|
redirect: 'follow',
|