@kitman-sh/cli 0.0.0-stage → 0.6.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.
@@ -0,0 +1,364 @@
1
+ // @bun
2
+ import {
3
+ build2,
4
+ provide2,
5
+ gen,
6
+ flatMap,
7
+ provideContext,
8
+ runForEach,
9
+ optionalKey,
10
+ optional,
11
+ Literal,
12
+ Null,
13
+ String2,
14
+ Boolean2,
15
+ Void,
16
+ Struct,
17
+ Record,
18
+ ArraySchema,
19
+ Union,
20
+ Literals,
21
+ UndefinedOr,
22
+ TaggedStruct,
23
+ Finite,
24
+ Int,
25
+ Uint8ArrayFromBase64,
26
+ TaggedError2,
27
+ Json,
28
+ WorkUnitRepoBinding,
29
+ WorkUnitClaimResult,
30
+ WorkUnitClaimInput,
31
+ WorkUnitHeartbeatInput,
32
+ WorkUnitHeartbeatResult,
33
+ WorkUnitReleaseInput,
34
+ WorkUnitCompletion
35
+ } from "./chunk-txyq8r41.js";
36
+ import {
37
+ layerWebSocketConstructorGlobal,
38
+ layerWebSocket,
39
+ make5,
40
+ make6,
41
+ layerJson,
42
+ make7,
43
+ withHeaders,
44
+ layerProtocolSocket,
45
+ Service
46
+ } from "./chunk-hw7qkap1.js";
47
+
48
+ // packages/live/src/rpc/runner-group.ts
49
+ var RunnerHeaders = { runnerId: "x-kitman-runner-id", protocolVersion: "x-kitman-rpc-protocol" };
50
+ var RunnerRpcProtocolVersion = 1;
51
+
52
+ class RunnerRpcError extends TaggedError2()("RunnerRpcError", {
53
+ code: String2,
54
+ message: String2,
55
+ status: Finite
56
+ }) {
57
+ }
58
+
59
+ class RunnerAuthentication extends Service()("RunnerAuthentication", {
60
+ error: RunnerRpcError
61
+ }) {
62
+ }
63
+ var EventFilterValue = Union([String2, Finite, Boolean2, Null]);
64
+ var WorkAvailable = TaggedStruct("WorkAvailable", {});
65
+ var LeaseRevoked = TaggedStruct("LeaseRevoked", { workUnitId: String2 });
66
+ var Settled = TaggedStruct("Settled", { workUnitId: String2 });
67
+ var Canceled = TaggedStruct("Canceled", { workUnitId: String2 });
68
+ var RunSettled = TaggedStruct("RunSettled", { runId: String2 });
69
+ var Steer = TaggedStruct("Steer", {
70
+ workUnitId: String2,
71
+ messageId: String2,
72
+ text: String2
73
+ });
74
+ var SessionUpdated = TaggedStruct("SessionUpdated", { workUnitId: String2 });
75
+ var RunnerSignal = Union([
76
+ WorkAvailable,
77
+ LeaseRevoked,
78
+ Settled,
79
+ Canceled,
80
+ RunSettled,
81
+ Steer,
82
+ SessionUpdated
83
+ ]);
84
+ var ProgramLease = Struct({
85
+ workUnitId: String2,
86
+ leaseOwnerId: String2,
87
+ runnerReplicaId: String2,
88
+ leaseId: String2,
89
+ attempt: Int,
90
+ fencingToken: String2,
91
+ capacitySlot: String2
92
+ });
93
+ var CheckpointSession = Struct({
94
+ harness: String2,
95
+ id: String2,
96
+ name: String2,
97
+ content: Uint8ArrayFromBase64
98
+ });
99
+ var StepCheckpoint = Struct({
100
+ ref: optionalKey(String2),
101
+ revision: optionalKey(String2),
102
+ session: optionalKey(CheckpointSession)
103
+ });
104
+ var SuggestedVerification = Struct({
105
+ name: String2,
106
+ command: String2,
107
+ workingDirectory: optional(String2)
108
+ });
109
+ var ProgramOutcome = Union([
110
+ Struct({ status: Literal("completed"), output: Json }),
111
+ Struct({ status: Literal("failed"), failureSummary: String2 })
112
+ ]);
113
+
114
+ class RunnerRpcs extends make6(make5("Claim", {
115
+ payload: WorkUnitClaimInput,
116
+ success: WorkUnitClaimResult,
117
+ error: RunnerRpcError
118
+ }), make5("Heartbeat", {
119
+ payload: WorkUnitHeartbeatInput,
120
+ success: WorkUnitHeartbeatResult,
121
+ error: RunnerRpcError
122
+ }), make5("Complete", {
123
+ payload: WorkUnitCompletion,
124
+ error: RunnerRpcError
125
+ }), make5("Release", {
126
+ payload: WorkUnitReleaseInput,
127
+ error: RunnerRpcError
128
+ }), make5("RecordActivity", {
129
+ payload: { lease: ProgramLease, name: String2, attempt: Int, exit: Json },
130
+ error: RunnerRpcError
131
+ }), make5("DeferredDone", {
132
+ payload: { lease: ProgramLease, name: String2, exit: Json },
133
+ error: RunnerRpcError
134
+ }), make5("ScheduleClock", {
135
+ payload: { lease: ProgramLease, name: String2, deferredName: String2, durationMillis: Int },
136
+ error: RunnerRpcError
137
+ }), make5("PutQuestion", {
138
+ payload: {
139
+ lease: ProgramLease,
140
+ step: String2,
141
+ prompt: String2,
142
+ options: optional(ArraySchema(String2)),
143
+ verification: optional(SuggestedVerification),
144
+ deferredName: String2
145
+ },
146
+ success: Struct({ questionId: String2 }),
147
+ error: RunnerRpcError
148
+ }), make5("StartAgentStep", {
149
+ payload: {
150
+ lease: ProgramLease,
151
+ step: String2,
152
+ deferredName: String2,
153
+ role: String2,
154
+ agentId: String2,
155
+ prompt: String2,
156
+ outputSchema: Json,
157
+ secrets: optionalKey(ArraySchema(String2))
158
+ },
159
+ success: Struct({ stepAttemptId: String2 }),
160
+ error: RunnerRpcError
161
+ }), make5("StartVerification", {
162
+ payload: {
163
+ lease: ProgramLease,
164
+ step: String2,
165
+ deferredName: String2,
166
+ revision: optionalKey(String2),
167
+ checks: optional(ArraySchema(String2))
168
+ },
169
+ success: Struct({ verificationId: String2 }),
170
+ error: RunnerRpcError
171
+ }), make5("KnownEvidence", {
172
+ payload: { lease: ProgramLease, trees: ArraySchema(String2) },
173
+ success: ArraySchema(Struct({ tree: String2, name: String2, command: String2 })),
174
+ error: RunnerRpcError
175
+ }), make5("AwaitEvent", {
176
+ payload: {
177
+ lease: ProgramLease,
178
+ step: String2,
179
+ deferredName: String2,
180
+ event: String2,
181
+ filter: optional(Record(String2, Union([EventFilterValue, ArraySchema(EventFilterValue)]))),
182
+ timeoutMillis: Int
183
+ },
184
+ error: RunnerRpcError
185
+ }), make5("AdmitChild", {
186
+ payload: {
187
+ lease: ProgramLease,
188
+ step: String2,
189
+ loop: String2,
190
+ input: Json,
191
+ deferredName: String2
192
+ },
193
+ success: Struct({ runId: String2 }),
194
+ error: RunnerRpcError
195
+ }), make5("StoreArtifact", {
196
+ payload: {
197
+ lease: ProgramLease,
198
+ step: String2,
199
+ kind: String2,
200
+ title: String2,
201
+ text: String2,
202
+ data: optional(Json)
203
+ },
204
+ success: Struct({ artifactId: String2 }),
205
+ error: RunnerRpcError
206
+ }), make5("RecordNote", {
207
+ payload: {
208
+ lease: ProgramLease,
209
+ step: String2,
210
+ text: String2,
211
+ artifactId: optional(String2)
212
+ },
213
+ error: RunnerRpcError
214
+ }), make5("PerformHostEffect", {
215
+ payload: {
216
+ lease: ProgramLease,
217
+ step: String2,
218
+ effect: Union([
219
+ Struct({ kind: Literal("post_comment"), changeRequestId: String2, body: String2 }),
220
+ Struct({
221
+ kind: Literal("change_request"),
222
+ title: String2,
223
+ body: String2,
224
+ integration: optionalKey(String2)
225
+ }),
226
+ Struct({
227
+ kind: Literal("write_back"),
228
+ milestone: optionalKey(Literals(["started", "change_request_opened", "merged", "failed"])),
229
+ changeRequestUrl: optionalKey(String2)
230
+ })
231
+ ])
232
+ },
233
+ success: UndefinedOr(Struct({ id: String2, number: String2, url: String2 })),
234
+ error: RunnerRpcError
235
+ }), make5("CreatePlanTasks", {
236
+ payload: {
237
+ lease: ProgramLease,
238
+ tickets: ArraySchema(Struct({
239
+ key: String2,
240
+ title: String2,
241
+ body: String2,
242
+ acceptanceCriteria: ArraySchema(String2),
243
+ blockedBy: ArraySchema(String2),
244
+ outcome: optionalKey(Literals(["feature", "bug", "refactor", "investigation", "docs", "migration"]))
245
+ }))
246
+ },
247
+ success: ArraySchema(Struct({ key: String2, taskId: String2 })),
248
+ error: RunnerRpcError
249
+ }), make5("IntegrationBranch", {
250
+ payload: { lease: ProgramLease, repoBindingId: optionalKey(String2) },
251
+ success: Struct({ repoBindingId: String2, branch: String2, revision: String2 }),
252
+ error: RunnerRpcError
253
+ }), make5("MergeChangeRequest", {
254
+ payload: { lease: ProgramLease, step: String2, changeRequestId: String2 },
255
+ success: Union([
256
+ Struct({ status: Literal("merged"), revision: String2 }),
257
+ Struct({ status: Literal("conflict") })
258
+ ]),
259
+ error: RunnerRpcError
260
+ }), make5("PublishRelease", {
261
+ payload: {
262
+ lease: ProgramLease,
263
+ step: String2,
264
+ tag: String2,
265
+ revision: optionalKey(String2),
266
+ title: String2,
267
+ notes: String2
268
+ },
269
+ success: Struct({ tag: String2, revision: String2, url: String2 }),
270
+ error: RunnerRpcError
271
+ }), make5("ReportSteer", {
272
+ payload: { workUnitId: String2, messageId: String2, delivered: Boolean2 },
273
+ success: Void,
274
+ error: RunnerRpcError
275
+ }), make5("SaveCheckpoint", {
276
+ payload: { lease: ProgramLease, checkpoint: StepCheckpoint },
277
+ success: Void,
278
+ error: RunnerRpcError
279
+ }), make5("ResumeCheckpoint", {
280
+ payload: { lease: ProgramLease },
281
+ success: UndefinedOr(StepCheckpoint),
282
+ error: RunnerRpcError
283
+ }), make5("SettledCheckpoints", {
284
+ success: ArraySchema(Struct({
285
+ stepAttemptId: String2,
286
+ ref: optionalKey(String2),
287
+ repoBinding: optionalKey(WorkUnitRepoBinding)
288
+ })),
289
+ error: RunnerRpcError
290
+ }), make5("ForgetCheckpoints", {
291
+ payload: { stepAttemptIds: ArraySchema(String2) },
292
+ success: Void,
293
+ error: RunnerRpcError
294
+ }), make5("Bundle", {
295
+ payload: { digest: String2 },
296
+ success: Struct({ bundle: String2 }),
297
+ error: RunnerRpcError
298
+ }), make5("SettledRuns", {
299
+ payload: { runIds: ArraySchema(String2) },
300
+ success: ArraySchema(String2),
301
+ error: RunnerRpcError
302
+ }), make5("ProgramSecrets", {
303
+ payload: { lease: ProgramLease },
304
+ success: Record(String2, String2),
305
+ error: RunnerRpcError
306
+ }), make5("Suspend", { payload: { lease: ProgramLease }, error: RunnerRpcError }), make5("FinishProgram", { payload: { lease: ProgramLease, outcome: ProgramOutcome }, error: RunnerRpcError }), make5("Signals", {
307
+ payload: { runnerReplicaId: String2 },
308
+ success: RunnerSignal,
309
+ error: RunnerRpcError,
310
+ stream: true
311
+ })).middleware(RunnerAuthentication) {
312
+ }
313
+
314
+ // packages/live/src/rpc/runner-client.ts
315
+ var runnerWebSocketProtocol2 = (url) => layerProtocolSocket({ retryTransientErrors: true }).pipe(provide2(layerWebSocket(url)), provide2(layerWebSocketConstructorGlobal), provide2(layerJson));
316
+ function runnerRpcUrl2(apiUrl) {
317
+ const url = new URL("/v1/runners/rpc", apiUrl);
318
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
319
+ return url.toString();
320
+ }
321
+ var makeRunnerRpcClient2 = (options) => gen(function* () {
322
+ const protocol = yield* build2(options.protocol);
323
+ const client = yield* make7(RunnerRpcs).pipe(provideContext(protocol));
324
+ const authorized = (call) => flatMap(options.bearer, (token) => withHeaders(call, {
325
+ authorization: `Bearer ${token}`,
326
+ [RunnerHeaders.runnerId]: options.runnerId,
327
+ [RunnerHeaders.protocolVersion]: String(RunnerRpcProtocolVersion)
328
+ }));
329
+ return {
330
+ claim: (input) => authorized(client.Claim(input)),
331
+ heartbeat: (input) => authorized(client.Heartbeat(input)),
332
+ complete: (input) => authorized(client.Complete(input)),
333
+ release: (input) => authorized(client.Release(input)),
334
+ recordActivity: (input) => authorized(client.RecordActivity(input)),
335
+ deferredDone: (input) => authorized(client.DeferredDone(input)),
336
+ scheduleClock: (input) => authorized(client.ScheduleClock(input)),
337
+ putQuestion: (input) => authorized(client.PutQuestion(input)),
338
+ programSecrets: (input) => authorized(client.ProgramSecrets(input)),
339
+ suspend: (input) => authorized(client.Suspend(input)),
340
+ finishProgram: (input) => authorized(client.FinishProgram(input)),
341
+ startAgentStep: (input) => authorized(client.StartAgentStep(input)),
342
+ startVerification: (input) => authorized(client.StartVerification(input)),
343
+ knownEvidence: (input) => authorized(client.KnownEvidence(input)),
344
+ awaitEvent: (input) => authorized(client.AwaitEvent(input)),
345
+ admitChild: (input) => authorized(client.AdmitChild(input)),
346
+ storeArtifact: (input) => authorized(client.StoreArtifact(input)),
347
+ recordNote: (input) => authorized(client.RecordNote(input)),
348
+ performHostEffect: (input) => authorized(client.PerformHostEffect(input)),
349
+ createPlanTasks: (input) => authorized(client.CreatePlanTasks(input)),
350
+ integrationBranch: (input) => authorized(client.IntegrationBranch(input)),
351
+ mergeChangeRequest: (input) => authorized(client.MergeChangeRequest(input)),
352
+ publishRelease: (input) => authorized(client.PublishRelease(input)),
353
+ bundle: (digest) => authorized(client.Bundle({ digest })),
354
+ settledRuns: (runIds) => authorized(client.SettledRuns({ runIds })),
355
+ reportSteer: (input) => authorized(client.ReportSteer(input)),
356
+ saveCheckpoint: (input) => authorized(client.SaveCheckpoint(input)),
357
+ resumeCheckpoint: (input) => authorized(client.ResumeCheckpoint(input)),
358
+ settledCheckpoints: authorized(client.SettledCheckpoints()),
359
+ forgetCheckpoints: (stepAttemptIds) => authorized(client.ForgetCheckpoints({ stepAttemptIds })),
360
+ listen: (runnerReplicaId, onSignal) => authorized(runForEach(client.Signals({ runnerReplicaId }), onSignal))
361
+ };
362
+ });
363
+
364
+ export { RunnerRpcProtocolVersion, RunnerRpcError, RunSettled, runnerWebSocketProtocol2, runnerRpcUrl2, makeRunnerRpcClient2 };
@@ -0,0 +1,107 @@
1
+ // Runs alone inside the egress proxy container, so it imports nothing outside Node.
2
+ import { once } from 'node:events'
3
+ import { connect, createServer, type Server, type Socket } from 'node:net'
4
+
5
+ /** Whether the allowlist takes `host:port`: an entry `host` takes any port, `host:port` one, `*.domain` subdomains. */
6
+ export const egressAllowed = (hosts: readonly string[], host: string, port: number) => {
7
+ const target = host.toLowerCase()
8
+
9
+ return hosts.some(entry => {
10
+ const [name = '', entryPort] = entry.toLowerCase().split(':')
11
+
12
+ if (entryPort !== undefined && Number(entryPort) !== port) return false
13
+
14
+ return name.startsWith('*.') ? target.endsWith(name.slice(1)) : target === name
15
+ })
16
+ }
17
+
18
+ const connectTarget = (authority: string) => {
19
+ const separator = authority.lastIndexOf(':')
20
+ const port = Number(authority.slice(separator + 1))
21
+
22
+ return separator === -1 || !Number.isInteger(port)
23
+ ? undefined
24
+ : { host: authority.slice(0, separator).replace(/^\[|\]$/g, ''), port }
25
+ }
26
+
27
+ const httpTarget = (absolute: string) => {
28
+ if (!URL.canParse(absolute)) return undefined
29
+ const url = new URL(absolute)
30
+
31
+ return url.protocol !== 'http:'
32
+ ? undefined
33
+ : {
34
+ host: url.hostname.replace(/^\[|\]$/g, ''),
35
+ port: url.port === '' ? 80 : Number(url.port),
36
+ path: `${url.pathname}${url.search}`
37
+ }
38
+ }
39
+
40
+ const answer = (socket: Socket, status: string) => socket.end(`HTTP/1.1 ${status}\r\nContent-Length: 0\r\n\r\n`)
41
+
42
+ const handle = (hosts: readonly string[]) => (client: Socket) => {
43
+ let head = Buffer.alloc(0)
44
+
45
+ const onData = (chunk: Buffer) => {
46
+ head = Buffer.concat([head, chunk])
47
+ const end = head.indexOf('\r\n\r\n')
48
+
49
+ if (end === -1) return head.length > 16_384 ? answer(client, '431 Request Header Fields Too Large') : undefined
50
+ client.off('data', onData)
51
+
52
+ const [requestLine = '', ...headers] = head.subarray(0, end).toString().split('\r\n')
53
+ const [method = '', target = '', version = 'HTTP/1.1'] = requestLine.split(' ')
54
+ const destination = method === 'CONNECT' ? connectTarget(target) : httpTarget(target)
55
+
56
+ if (destination === undefined) return answer(client, '405 Method Not Allowed')
57
+
58
+ if (!egressAllowed(hosts, destination.host, destination.port)) return answer(client, '403 Forbidden')
59
+
60
+ const upstream = connect(destination.port, destination.host, () => {
61
+ if (!('path' in destination)) {
62
+ client.write('HTTP/1.1 200 Connection Established\r\n\r\n')
63
+ upstream.write(head.subarray(end + 4))
64
+ } else {
65
+ // One request per connection: a reused one could carry a request for another host to this one.
66
+ const kept = headers.filter(line => !/^(proxy-)?connection:/i.test(line))
67
+
68
+ upstream.write(
69
+ Buffer.concat([
70
+ Buffer.from(
71
+ [[method, destination.path, version].join(' '), ...kept, 'Connection: close', '', ''].join('\r\n')
72
+ ),
73
+ head.subarray(end + 4)
74
+ ])
75
+ )
76
+ }
77
+
78
+ client.pipe(upstream)
79
+ upstream.pipe(client)
80
+ })
81
+
82
+ upstream.on('error', () => (client.writable ? answer(client, '502 Bad Gateway') : client.destroy()))
83
+ client.on('error', () => upstream.destroy())
84
+ }
85
+
86
+ client.on('data', onData)
87
+ client.on('error', () => client.destroy())
88
+ }
89
+
90
+ /** An HTTP proxy that tunnels CONNECT and forwards plain HTTP only to the allowlist. */
91
+ export const startEgressProxy = (input: {
92
+ readonly hosts: readonly string[]
93
+ readonly port: number
94
+ readonly hostname: string
95
+ }): Promise<Server> => {
96
+ const server = createServer(handle(input.hosts)).listen(input.port, input.hostname)
97
+
98
+ return once(server, 'listening').then(() => server)
99
+ }
100
+
101
+ // `bun egress-proxy.ts <comma-separated allowlist>`
102
+ if (import.meta.main)
103
+ void startEgressProxy({
104
+ hosts: (Bun.argv[2] ?? '').split(',').filter(host => host.length > 0),
105
+ port: 3128,
106
+ hostname: '0.0.0.0'
107
+ })