@lythos/cold-pool 0.13.2 → 0.13.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lythos/cold-pool",
3
- "version": "0.13.2",
3
+ "version": "0.13.3",
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.13.2",
45
+ "@lythos/infra": "^0.13.3",
46
46
  "simple-git": "^3.36.0"
47
47
  },
48
48
  "engines": {
package/src/cold-pool.ts CHANGED
@@ -137,9 +137,8 @@ export class ColdPool {
137
137
  const sub = join(dir, d.name)
138
138
  if (existsSync(join(sub, 'SKILL.md'))) {
139
139
  push(sub)
140
- } else {
141
- walk(sub, push)
142
140
  }
141
+ walk(sub, push)
143
142
  }
144
143
  }
145
144
 
@@ -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
- const res = await fetch(targetUrl, {
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',