返回 CodeWhale
index.mjs
1 // Ambient Node file access from inside a host plugin. Under the host sandbox
2 // the read of a Codewhale secret must fail even though the plugin is trusted.
3 import { readFile, writeFile, readlink } from 'node:fs/promises'
4 import { connect } from 'node:net'
5 import { createSocket } from 'node:dgram'
6
7 export const name = 'secret-probe'
8 export const inject = ['tools']
9
10 export function apply(ctx) {
11 ctx.tools.register({
12 name: 'probe_read',
13 description: 'Read a file with node:fs and report what happened.',
14 parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
15 async execute(args) {
16 try {
17 return { ok: true, text: await readFile(args.path, 'utf8') }
18 } catch (error) {
19 return { ok: false, code: error.code ?? String(error) }
20 }
21 },
22 })
23 ctx.tools.register({
24 name: 'probe_write',
25 description: 'Write a file with node:fs and report what happened.',
26 parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
27 async execute(args) {
28 try {
29 await writeFile(args.path, 'probe')
30 return { ok: true }
31 } catch (error) {
32 return { ok: false, code: error.code ?? String(error) }
33 }
34 },
35 })
36 ctx.tools.register({
37 name: 'probe_connect',
38 description: 'Connect to the test controller loopback listener and report the OS result.',
39 parameters: { type: 'object', properties: { port: { type: 'integer', minimum: 1, maximum: 65535 } }, required: ['port'] },
40 async execute(args) {
41 // Linux's private loopback cannot reach the controller's listener.
42 // Report the actual kernel namespace, rather than infer isolation from errno.
43 const networkNamespace = process.platform === 'linux'
44 ? await readlink('/proc/self/ns/net')
45 : undefined
46 return new Promise((resolve) => {
47 const socket = connect({ host: '127.0.0.1', port: args.port })
48 const finish = (result) => {
49 socket.destroy()
50 resolve(networkNamespace === undefined ? result : { ...result, network_namespace: networkNamespace })
51 }
52 socket.once('connect', () => finish({ ok: true }))
53 socket.once('error', (error) => finish({ ok: false, code: error.code ?? String(error) }))
54 socket.setTimeout(1000, () => finish({ ok: false, code: 'ETIMEDOUT' }))
55 })
56 },
57 })
58 ctx.tools.register({
59 name: 'probe_send',
60 description: 'Send a datagram to the test controller and report the unmodified OS result.',
61 parameters: { type: 'object', properties: { port: { type: 'integer', minimum: 1, maximum: 65535 } }, required: ['port'] },
62 async execute(args) {
63 return new Promise((resolve) => {
64 const socket = createSocket('udp4')
65 let finished = false
66 const finish = (result) => {
67 if (finished) return
68 finished = true
69 clearTimeout(timer)
70 // A refused bind leaves the socket unopened; cleanup must not replace
71 // the original access-denied result with ERR_SOCKET_DGRAM_NOT_RUNNING.
72 try { socket.close() } catch {}
73 resolve(result)
74 }
75 const failure = (error) => finish({ ok: false, code: error.code ?? String(error), syscall: error.syscall ?? null })
76 const timer = setTimeout(() => finish({ ok: false, code: 'ETIMEDOUT' }), 1000)
77 socket.once('error', failure)
78 try {
79 socket.send('native-network-probe', args.port, '127.0.0.1', (error) => {
80 if (error) failure(error)
81 else finish({ ok: true })
82 })
83 } catch (error) { failure(error) }
84 })
85 },
86 })
87 }
88
88 lines Plain Text