@kitman-sh/cli 0.10.71 → 0.10.73

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.
@@ -285,6 +285,16 @@ import {
285
285
  skillIdentity2
286
286
  } from "./chunk-4wqjxfbv.js";
287
287
  import"./chunk-hr9vd6dj.js";
288
+ import {
289
+ egressAllowed,
290
+ writeSecretRegistry,
291
+ secretRefusalLog,
292
+ appendSecretRefusals,
293
+ parseSecretRefusals,
294
+ secretFilter2,
295
+ opaqueTunnelRefusal2,
296
+ isOpaqueConnectReason2
297
+ } from "./chunk-3edjgnxf.js";
288
298
  import {
289
299
  sliceBytes2,
290
300
  maxLineBytes2,
@@ -293,11 +303,6 @@ import {
293
303
  decoded2,
294
304
  readOpencode2
295
305
  } from "./chunk-csbf77zs.js";
296
- import {
297
- writeSecretRegistry,
298
- secretRefusalLog,
299
- parseSecretRefusals
300
- } from "./chunk-1w81w56v.js";
301
306
  import {
302
307
  uploadRunFile,
303
308
  programLease,
@@ -2594,6 +2599,82 @@ var prepareDockerHome = (input) => gen(function* () {
2594
2599
  }
2595
2600
  });
2596
2601
 
2602
+ // packages/agent-runtime/src/docker-secret-proxy.ts
2603
+ import { createHttpProxyServer } from "@anthropic-ai/sandbox-runtime/dist/sandbox/http-proxy.js";
2604
+ import { isLoopbackName } from "@anthropic-ai/sandbox-runtime/dist/sandbox/address.js";
2605
+ import { createMitmCA, disposeMitmCA } from "@anthropic-ai/sandbox-runtime/dist/sandbox/mitm-ca.js";
2606
+ import { canonicalizeHost } from "@anthropic-ai/sandbox-runtime/dist/sandbox/parent-proxy.js";
2607
+ import {
2608
+ createResolvedAddressGuard
2609
+ } from "@anthropic-ai/sandbox-runtime/dist/sandbox/resolved-address-guard.js";
2610
+ import { chmodSync } from "fs";
2611
+ import { isIP } from "net";
2612
+ import { networkInterfaces } from "os";
2613
+ class SecretProxyUnavailable extends TaggedError("SecretProxyUnavailable") {
2614
+ }
2615
+ var dockerSecretProxySocket = (registryDirectory) => `${registryDirectory}/proxy.sock`;
2616
+ var anyName = "kitman-target.invalid";
2617
+ var dockerBridge = (name) => name === "docker0" || name === "docker_gwbridge" || name.startsWith("br-");
2618
+ var dockerNetworkSubnets = (interfaces = networkInterfaces()) => Object.entries(interfaces).flatMap(([name, addresses]) => dockerBridge(name) ? (addresses ?? []).flatMap(({ cidr }) => cidr === null ? [] : [cidr]) : []);
2619
+ var runnerAddresses = (interfaces) => {
2620
+ let current;
2621
+ try {
2622
+ current = interfaces();
2623
+ } catch {
2624
+ return [];
2625
+ }
2626
+ const own = Object.values(current).flatMap((addresses) => (addresses ?? []).map(({ address }) => address));
2627
+ return [...own, ...dockerNetworkSubnets(current)];
2628
+ };
2629
+ var runnerAddressGuard = (options = {}, interfaces = networkInterfaces) => createResolvedAddressGuard({ ...options, localAddresses: () => runnerAddresses(interfaces) });
2630
+ var namesRunner = (guard, host, port) => {
2631
+ const canonical = canonicalizeHost(host) ?? host;
2632
+ if (isLoopbackName(canonical))
2633
+ return !guard.permits(anyName, "127.0.0.1", port);
2634
+ return isIP(canonical) !== 0 && !guard.permits(anyName, canonical, port);
2635
+ };
2636
+ var startDockerSecretProxy = (input) => gen(function* () {
2637
+ const ca = yield* acquireRelease(try_2({ try: () => createMitmCA({}), catch: (cause) => new SecretProxyUnavailable({ cause }) }), (ca) => ignore(tryPromise(() => disposeMitmCA(ca))));
2638
+ const { network, registryFile } = input;
2639
+ const guard = runnerAddressGuard(input.addressGuard);
2640
+ const server = createHttpProxyServer({
2641
+ filter: (port, host, _socket, _command, explain) => {
2642
+ if (namesRunner(guard, host, port)) {
2643
+ explain?.("the Runner's own address or one of its docker networks");
2644
+ return false;
2645
+ }
2646
+ if (network === "open" || egressAllowed(network.hosts, host, port))
2647
+ return true;
2648
+ explain?.("host not allowed");
2649
+ return false;
2650
+ },
2651
+ lookupFor: (port) => guard.lookupFor(port),
2652
+ mitmCA: ca,
2653
+ filterRequest: secretFilter2(registryFile),
2654
+ refuseOpaqueTunnels: true,
2655
+ onFilterRequestDenied: ({ method, url, reason }) => {
2656
+ if (method !== "CONNECT" || !isOpaqueConnectReason2(reason))
2657
+ return;
2658
+ try {
2659
+ appendSecretRefusals(registryFile, [{ host: url, reason: opaqueTunnelRefusal2 }]);
2660
+ } catch {}
2661
+ },
2662
+ ...input.upstreamCA === undefined ? undefined : { tlsTerminateUpstreamCA: input.upstreamCA }
2663
+ });
2664
+ yield* acquireRelease(callback3((resume) => {
2665
+ server.once("error", (cause) => resume(fail7(new SecretProxyUnavailable({ cause }))));
2666
+ server.listen(input.socket, () => resume(void_3));
2667
+ }), () => sync2(() => server.close()));
2668
+ yield* try_2({
2669
+ try: () => chmodSync(input.socket, 438),
2670
+ catch: (cause) => new SecretProxyUnavailable({ cause })
2671
+ });
2672
+ return { socket: input.socket, trustBundle: ca.trustBundlePath };
2673
+ });
2674
+
2675
+ // packages/agent-runtime/src/docker-sandbox.ts
2676
+ import { CA_TRUST_VARS } from "@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-utils.js";
2677
+
2597
2678
  // packages/agent-runtime/src/helper-scripts.ts
2598
2679
  import { fileURLToPath } from "url";
2599
2680
  var helperScriptPath = (name) => gen(function* () {
@@ -2611,9 +2692,14 @@ var RunnerLabel = "kitman.runner";
2611
2692
  var StepAttemptLabel = "kitman.step-attempt";
2612
2693
  var EgressProxyImage = "oven/bun@sha256:4cf82f5686b196504fe119db3fcd9185b191f82ece82c40680e597228ec6070a";
2613
2694
  var egressProxyAddress = "http://kitman-egress:3128";
2695
+ var relayedSocket = "/kitman/proxy.sock";
2696
+ var trustBundle = "/kitman/trust.pem";
2614
2697
 
2615
2698
  class ContainerError extends TaggedError("ContainerError") {
2616
2699
  }
2700
+
2701
+ class SecretProxyUnreachable extends TaggedError("SecretProxyUnreachable") {
2702
+ }
2617
2703
  var imagePull = { timeout: "30 minutes" };
2618
2704
  var dockerContainerRunner = map8(ChildProcessSpawner, (spawner) => {
2619
2705
  const docker = (args, options = {}) => runCommand2("docker", args, options).pipe(mapError2((error) => new ContainerError({ message: error.message })), provideService(ChildProcessSpawner, spawner));
@@ -2629,6 +2715,7 @@ var dockerContainerRunner = map8(ChildProcessSpawner, (spawner) => {
2629
2715
  input.name,
2630
2716
  ...labelArgs(input.labels),
2631
2717
  ...input.network === undefined ? [] : ["--network", input.network],
2718
+ ...input.alias === undefined ? [] : ["--network-alias", input.alias],
2632
2719
  ...input.user === undefined ? [] : ["--user", input.user],
2633
2720
  ...input.mounts.flatMap((mount) => [
2634
2721
  "--volume",
@@ -2678,26 +2765,49 @@ var startDockerSandbox = (input) => gen(function* () {
2678
2765
  [StepAttemptLabel, input.attemptId]
2679
2766
  ];
2680
2767
  const agent = `kitman-agent-${id}`;
2681
- const network = input.policy.network === "open" ? undefined : `kitman-agent-net-${id}`;
2768
+ const { secretProxy } = input;
2769
+ const network = input.policy.network === "open" && secretProxy === undefined ? undefined : `kitman-agent-net-${id}`;
2682
2770
  yield* acquireRelease(void_3, () => containers.removeLabelled([StepAttemptLabel, input.attemptId]));
2683
- if (input.policy.network !== "open" && network !== undefined) {
2771
+ if (network !== undefined) {
2684
2772
  const proxy = `kitman-egress-${id}`;
2773
+ const script = {
2774
+ source: yield* helperScriptPath("egress-proxy.ts"),
2775
+ target: "/kitman/egress-proxy.ts",
2776
+ readOnly: true
2777
+ };
2685
2778
  yield* containers.createInternalNetwork({ name: network, labels });
2686
- yield* containers.run({
2687
- name: proxy,
2688
- image: input.docker.proxyImage ?? EgressProxyImage,
2689
- labels,
2690
- mounts: [
2691
- {
2692
- source: yield* helperScriptPath("egress-proxy.ts"),
2693
- target: "/kitman/egress-proxy.ts",
2694
- readOnly: true
2695
- }
2696
- ],
2697
- env: {},
2698
- command: ["bun", "/kitman/egress-proxy.ts", input.policy.network.hosts.join(",")]
2699
- });
2700
- yield* containers.connect({ network, container: proxy, alias: "kitman-egress" });
2779
+ const image = input.docker.proxyImage ?? EgressProxyImage;
2780
+ if (secretProxy === undefined) {
2781
+ yield* containers.run({
2782
+ name: proxy,
2783
+ image,
2784
+ labels,
2785
+ mounts: [script],
2786
+ env: {},
2787
+ command: ["bun", script.target, input.policy.network === "open" ? "" : input.policy.network.hosts.join(",")]
2788
+ });
2789
+ yield* containers.connect({ network, container: proxy, alias: "kitman-egress" });
2790
+ } else {
2791
+ yield* containers.run({
2792
+ name: proxy,
2793
+ image,
2794
+ labels,
2795
+ network,
2796
+ alias: "kitman-egress",
2797
+ mounts: [script, { source: secretProxy.socket, target: relayedSocket }],
2798
+ env: {},
2799
+ command: ["bun", script.target, "--relay", relayedSocket]
2800
+ });
2801
+ const probe = yield* containers.exec({
2802
+ container: proxy,
2803
+ workdir: "/",
2804
+ env: {},
2805
+ command: ["bun", script.target, "--probe", relayedSocket],
2806
+ timeout: "1 minute"
2807
+ });
2808
+ if (probe.exitCode !== 0)
2809
+ return yield* new SecretProxyUnreachable({ output: probe.output });
2810
+ }
2701
2811
  for (const service of input.services ?? [])
2702
2812
  yield* acquireRelease(containers.connect({ network, ...service }), () => containers.disconnect({ network, container: service.container }));
2703
2813
  }
@@ -2709,7 +2819,8 @@ var startDockerSandbox = (input) => gen(function* () {
2709
2819
  ...input.docker.user === undefined ? undefined : { user: input.docker.user },
2710
2820
  mounts: [
2711
2821
  ...input.policy.writablePaths.map((writable) => ({ source: writable, target: writable })),
2712
- ...(input.readOnlyPaths ?? []).map((readable) => ({ source: readable, target: readable, readOnly: true }))
2822
+ ...(input.readOnlyPaths ?? []).map((readable) => ({ source: readable, target: readable, readOnly: true })),
2823
+ ...secretProxy === undefined ? [] : [{ source: secretProxy.trustBundle, target: trustBundle, readOnly: true }]
2713
2824
  ],
2714
2825
  workdir: input.workspace,
2715
2826
  env: {
@@ -2722,7 +2833,8 @@ var startDockerSandbox = (input) => gen(function* () {
2722
2833
  http_proxy: egressProxyAddress,
2723
2834
  NO_PROXY: "localhost,127.0.0.1",
2724
2835
  no_proxy: "localhost,127.0.0.1"
2725
- }
2836
+ },
2837
+ ...secretProxy === undefined ? undefined : Object.fromEntries(CA_TRUST_VARS.map((name) => [name, trustBundle]))
2726
2838
  },
2727
2839
  command: ["sleep", "infinity"]
2728
2840
  });
@@ -3179,12 +3291,14 @@ var runAttempt = (request, options) => gen(function* () {
3179
3291
  const { attempt } = request.attempt;
3180
3292
  const workspace = request.workspace.path;
3181
3293
  const secrets = request.launch.secrets ?? {};
3182
- if (Object.keys(secrets).length > 0 && request.sandbox.backend === "docker")
3183
- return failed("secrets_need_srt", "A docker Step cannot take Secrets yet: its egress proxy cannot swap their references for values, and Kitman puts no value in a sandbox. Run this Step in srt.");
3184
3294
  const bound = yield* result(requireSecretHosts(request.launch.secrets, request.launch.secretDestinations));
3185
3295
  if (isFailure(bound))
3186
3296
  return failed(bound.failure.reason, bound.failure.summary);
3187
- const referenced = request.sandbox.backend === "srt" ? secretReferences(secrets, request.launch.secretDestinations ?? {}) : undefined;
3297
+ const referenced = request.sandbox.backend === "host" ? undefined : secretReferences(secrets, request.launch.secretDestinations ?? {});
3298
+ const holdSecrets = (initial) => stepSecretRegistry(stepSecretDirectory(options.home, attempt.id), initial, referenced?.references ?? {}).pipe(mapError2(() => new AgentStepFailure({
3299
+ reason: "secret_registry_unwritable",
3300
+ summary: "The Step's Secret references could not be written."
3301
+ })));
3188
3302
  const agent = referenced === undefined || request.launch.secrets === undefined ? request.launch : { ...request.launch, secrets: referenced.references };
3189
3303
  const prepared = yield* result(prepareHarness({
3190
3304
  attempt: request.attempt,
@@ -3225,10 +3339,7 @@ var runAttempt = (request, options) => gen(function* () {
3225
3339
  ]))
3226
3340
  };
3227
3341
  const holdsLogin = holdsLoginByReference(launch);
3228
- const registry = Object.keys(initial).length === 0 && !holdsLogin ? undefined : yield* stepSecretRegistry(stepSecretDirectory(options.home, attempt.id), initial, referenced?.references ?? {}).pipe(mapError2(() => new AgentStepFailure({
3229
- reason: "secret_registry_unwritable",
3230
- summary: "The Step's Secret references could not be written."
3231
- })));
3342
+ const registry = Object.keys(initial).length === 0 && !holdsLogin ? undefined : yield* holdSecrets(initial);
3232
3343
  const withSessions = sessions === undefined ? policy : {
3233
3344
  ...policy,
3234
3345
  writablePaths: [...policy.writablePaths, sessions],
@@ -3254,14 +3365,27 @@ var runAttempt = (request, options) => gen(function* () {
3254
3365
  case "docker": {
3255
3366
  if (options.docker === undefined)
3256
3367
  return yield* new AgentStepFailure({ reason: "sandbox_unavailable", summary: "`docker` is not available." });
3368
+ const registry = referenced === undefined || Object.keys(referenced.registry).length === 0 ? undefined : yield* holdSecrets(referenced.registry);
3369
+ const secretProxy = registry === undefined ? undefined : yield* startDockerSecretProxy({
3370
+ registryFile: registry.file,
3371
+ socket: dockerSecretProxySocket(registry.directory),
3372
+ network: policy.network
3373
+ }).pipe(tapError2((error) => logWarning("secret_proxy_unavailable", error.cause)), mapError2(() => new AgentStepFailure({
3374
+ reason: "secret_proxy_unavailable",
3375
+ summary: "The proxy that swaps the Step's Secret references could not be started."
3376
+ })));
3257
3377
  const { container } = yield* startDockerSandbox({
3258
3378
  docker: options.docker,
3259
3379
  runnerId: request.runnerId,
3260
3380
  attemptId: attempt.id,
3261
3381
  policy,
3262
3382
  workspace,
3263
- configDir
3264
- }).pipe(mapError2(() => new AgentStepFailure({
3383
+ configDir,
3384
+ ...secretProxy === undefined ? undefined : { secretProxy }
3385
+ }).pipe(tapError2((error) => isTagged(error, "SecretProxyUnreachable") ? logWarning("secret_proxy_unavailable", error.output) : void_3), mapError2((error) => isTagged(error, "SecretProxyUnreachable") ? new AgentStepFailure({
3386
+ reason: "secret_proxy_unavailable",
3387
+ summary: "The egress container could not reach the proxy that swaps the Step's Secret references."
3388
+ }) : new AgentStepFailure({
3265
3389
  reason: "sandbox_container_unavailable",
3266
3390
  summary: "The attempt's container could not be started."
3267
3391
  })));
@@ -3285,7 +3409,7 @@ var runAttempt = (request, options) => gen(function* () {
3285
3409
  return {
3286
3410
  argv: dockerExecArgv({ container, workspace, env: policy.environment, argv: harnessArgv }),
3287
3411
  env: policy.environment,
3288
- registry: undefined
3412
+ registry
3289
3413
  };
3290
3414
  }
3291
3415
  default:
@@ -6804,6 +6928,7 @@ export {
6804
6928
  RepoMaterializationError,
6805
6929
  RunnerLabel,
6806
6930
  RunnerReplicaCancellation,
6931
+ SecretProxyUnreachable,
6807
6932
  SkillMaterializationRefused2 as SkillMaterializationRefused,
6808
6933
  StepAttemptLabel,
6809
6934
  agentStepExecutionLimit,
@@ -0,0 +1,202 @@
1
+ // @bun
2
+ import {
3
+ getOrUndefined,
4
+ flatMap,
5
+ toArray,
6
+ decodeUnknownOption,
7
+ optionalKey,
8
+ String4,
9
+ Struct,
10
+ Record,
11
+ ArraySchema,
12
+ decodeJsonStringOption,
13
+ encodeUnknownJsonStringSync
14
+ } from "./chunk-d3ksv8y1.js";
15
+ import {
16
+ replaceSecretReferences,
17
+ secretReferencesIn
18
+ } from "./chunk-rrnee9mk.js";
19
+
20
+ // packages/agent-runtime/src/srt-step.ts
21
+ import { SandboxRuntimeConfigSchema } from "@anthropic-ai/sandbox-runtime";
22
+ import { spawn } from "child_process";
23
+ import { readFileSync as readFileSync2 } from "fs";
24
+
25
+ // packages/agent-runtime/src/secret-proxy.ts
26
+ import { appendFileSync, readFileSync, renameSync, writeFileSync } from "fs";
27
+
28
+ // packages/agent-runtime/src/egress-proxy.ts
29
+ var egressAllowed = (hosts, host, port) => {
30
+ const target = host.toLowerCase();
31
+ return hosts.some((entry) => {
32
+ const [name = "", entryPort] = entry.toLowerCase().split(":");
33
+ if (entryPort !== undefined && Number(entryPort) !== port)
34
+ return false;
35
+ return name.startsWith("*.") ? target.endsWith(name.slice(1)) : target === name;
36
+ });
37
+ };
38
+ if (false)
39
+ ;
40
+
41
+ // packages/agent-runtime/src/secret-proxy.ts
42
+ var basicPrefix = /^basic\s+/i;
43
+ var basicCredential = (value) => {
44
+ if (!basicPrefix.test(value))
45
+ return;
46
+ const decoded = Buffer.from(value.replace(basicPrefix, ""), "base64").toString("utf8");
47
+ return secretReferencesIn(decoded).length === 0 ? undefined : decoded;
48
+ };
49
+ var referencesInHeader = (value) => [
50
+ ...new Set([...secretReferencesIn(value), ...secretReferencesIn(basicCredential(value) ?? "")])
51
+ ];
52
+ var swapped = (value, registry) => {
53
+ const swap = (reference) => registry[reference]?.value ?? reference;
54
+ const basic = basicCredential(value);
55
+ return basic === undefined ? replaceSecretReferences(value, swap) : `Basic ${Buffer.from(replaceSecretReferences(basic, swap)).toString("base64")}`;
56
+ };
57
+ var decideSecretRequest = (request, readRegistry) => {
58
+ const carrying = request.headers.flatMap(([name, value]) => {
59
+ const references = referencesInHeader(value);
60
+ return references.length === 0 ? [] : [{ name, value, references }];
61
+ });
62
+ const inTarget = secretReferencesIn(request.target);
63
+ if (carrying.length === 0 && inTarget.length === 0)
64
+ return { action: "allow" };
65
+ const registry = readRegistry();
66
+ const refusals = [
67
+ ...inTarget.map((reference) => ({
68
+ reference,
69
+ host: request.host,
70
+ reason: "A reference is swapped only in a header, never in a URL."
71
+ })),
72
+ ...[...new Set(carrying.flatMap((header) => header.references))].flatMap((reference) => {
73
+ const entry = registry?.[reference];
74
+ const reason = entry === undefined ? "The reference is unknown or revoked." : request.scheme !== "https" ? "A reference is swapped only over HTTPS." : !egressAllowed(entry.hosts, request.host, request.port) ? `The Secret may be sent only to ${entry.hosts.join(", ")}.` : undefined;
75
+ return reason === undefined ? [] : [{ reference, host: request.host, reason }];
76
+ })
77
+ ];
78
+ if (refusals.length > 0)
79
+ return {
80
+ action: "deny",
81
+ reason: `Kitman refused this request: ${refusals.map((refusal) => `${refusal.reference}: ${refusal.reason}`).join(" ")}`,
82
+ refusals
83
+ };
84
+ const names = new Set(carrying.map((header) => header.name.toLowerCase()));
85
+ return {
86
+ action: "allow",
87
+ setHeaders: request.headers.filter(([name]) => names.has(name.toLowerCase())).map(([name, value]) => [name, registry === undefined ? value : swapped(value, registry)])
88
+ };
89
+ };
90
+ var Registry = Record(String4, Struct({ value: String4, hosts: ArraySchema(String4) }));
91
+ var Refusal = Struct({
92
+ reference: optionalKey(String4),
93
+ host: String4,
94
+ reason: String4
95
+ });
96
+ var readSecretRegistry = (file) => {
97
+ try {
98
+ return getOrUndefined(flatMap(decodeJsonStringOption(readFileSync(file, "utf8")), decodeUnknownOption(Registry)));
99
+ } catch {
100
+ return;
101
+ }
102
+ };
103
+ var writeSecretRegistry = (file, registry) => {
104
+ const next = `${file}.next`;
105
+ writeFileSync(next, encodeUnknownJsonStringSync(registry), { mode: 384 });
106
+ renameSync(next, file);
107
+ };
108
+ var secretRefusalLog = (registryFile) => `${registryFile}.refused`;
109
+ var appendSecretRefusals = (registryFile, refusals) => appendFileSync(secretRefusalLog(registryFile), refusals.map((refusal) => `${encodeUnknownJsonStringSync(refusal)}
110
+ `).join(""), { mode: 384 });
111
+ var parseSecretRefusals = (text) => text.split(`
112
+ `).flatMap((line) => toArray(flatMap(decodeJsonStringOption(line), decodeUnknownOption(Refusal))));
113
+
114
+ // packages/agent-runtime/src/srt-step.ts
115
+ var secretFilter2 = (registryFile) => (request, info) => {
116
+ const url = new URL(request.url);
117
+ const decision = decideSecretRequest({
118
+ headers: [...request.headers.entries()],
119
+ host: info?.target?.host ?? url.hostname,
120
+ port: info?.target?.port ?? (url.port === "" ? url.protocol === "https:" ? 443 : 80 : Number(url.port)),
121
+ target: info?.requestTarget ?? `${url.pathname}${url.search}`,
122
+ scheme: info?.scheme ?? (url.protocol === "https:" ? "https" : "http")
123
+ }, () => readSecretRegistry(registryFile));
124
+ if (decision.action === "allow")
125
+ return Promise.resolve(decision.setHeaders === undefined ? { action: "allow" } : { action: "allow", setHeaders: decision.setHeaders.map(([name, value]) => [name, value]) });
126
+ try {
127
+ appendSecretRefusals(registryFile, decision.refusals);
128
+ } catch {}
129
+ return Promise.resolve({ action: "deny", status: 403, mark: "secret-refused", reason: decision.reason });
130
+ };
131
+ var stepSandboxConfig = (settings, registryFile) => {
132
+ const parsed = SandboxRuntimeConfigSchema.safeParse({
133
+ ...settings,
134
+ network: { ...settings.network, tlsTerminate: {}, refuseOpaqueTunnels: true }
135
+ });
136
+ return parsed.success ? {
137
+ success: true,
138
+ data: { ...parsed.data, network: { ...parsed.data.network, filterRequest: secretFilter2(registryFile) } }
139
+ } : parsed;
140
+ };
141
+ var opaqueTunnelRefusal2 = "A tunnel Kitman cannot see into, such as SOCKS or a CONNECT without TLS, is refused.";
142
+ var opaqueConnectReasons = "tunnel carries no TLS|this proxy does not tunnel CONNECTs";
143
+ var isOpaqueConnectReason2 = (reason) => new RegExp(`^(?:${opaqueConnectReasons})`).test(reason);
144
+ var opaqueTunnel = new RegExp(`^deny (?:socks-tunnel (\\S+) \\(refuseOpaqueTunnels\\)|http-request CONNECT (\\S+) \\((?:${opaqueConnectReasons})[^)]*\\))`);
145
+ var refusedTunnelHost = (line) => {
146
+ const match = opaqueTunnel.exec(line);
147
+ return match === null ? undefined : match[1] ?? match[2];
148
+ };
149
+ var shellQuoted = (arg) => `'${arg.replaceAll("'", "'\\''")}'`;
150
+ var argument = (args, flag) => {
151
+ const at = args.indexOf(flag);
152
+ return at === -1 ? undefined : args[at + 1];
153
+ };
154
+ var runSrtStep2 = async (args) => {
155
+ const separator = args.indexOf("--");
156
+ const settingsFile = argument(args, "--settings");
157
+ const registryFile = argument(args, "--secrets");
158
+ const command = separator === -1 ? [] : args.slice(separator + 1);
159
+ if (settingsFile === undefined || registryFile === undefined || command.length === 0) {
160
+ console.error("Usage: srt-step --settings <file> --secrets <registry> -- <command>");
161
+ process.exit(2);
162
+ }
163
+ delete process.env.SRT_DEBUG;
164
+ const { SandboxManager } = await import("@anthropic-ai/sandbox-runtime");
165
+ const config = stepSandboxConfig(JSON.parse(readFileSync2(settingsFile, "utf8")), registryFile);
166
+ if (!config.success) {
167
+ console.error(`srt-step: ${settingsFile} does not hold a valid srt config: ${config.error.message}`);
168
+ process.exit(2);
169
+ }
170
+ await SandboxManager.initialize(config.data);
171
+ const recorded = new WeakSet;
172
+ SandboxManager.getSandboxViolationStore().subscribe((violations) => {
173
+ const refusals = violations.flatMap((violation) => {
174
+ if (recorded.has(violation))
175
+ return [];
176
+ recorded.add(violation);
177
+ const host = refusedTunnelHost(violation.line);
178
+ return host === undefined ? [] : [{ host, reason: opaqueTunnelRefusal2 }];
179
+ });
180
+ if (refusals.length === 0)
181
+ return;
182
+ try {
183
+ appendSecretRefusals(registryFile, refusals);
184
+ } catch {}
185
+ });
186
+ const child = spawn(await SandboxManager.wrapWithSandbox(command.map(shellQuoted).join(" ")), {
187
+ shell: true,
188
+ stdio: "inherit"
189
+ });
190
+ child.on("exit", (code, signal) => {
191
+ SandboxManager.cleanupAfterCommand();
192
+ process.exit(signal === null ? code ?? 0 : signal === "SIGINT" || signal === "SIGTERM" ? 0 : 1);
193
+ });
194
+ child.on("error", (error) => {
195
+ console.error(`Failed to execute command: ${error.message}`);
196
+ process.exit(1);
197
+ });
198
+ for (const signal of ["SIGINT", "SIGTERM"])
199
+ process.on(signal, () => child.kill(signal));
200
+ };
201
+
202
+ export { egressAllowed, writeSecretRegistry, secretRefusalLog, appendSecretRefusals, parseSecretRefusals, secretFilter2, opaqueTunnelRefusal2, isOpaqueConnectReason2, runSrtStep2 };
@@ -0,0 +1,16 @@
1
+ // @bun
2
+ import {
3
+ secretFilter2,
4
+ opaqueTunnelRefusal2,
5
+ isOpaqueConnectReason2,
6
+ runSrtStep2
7
+ } from "./chunk-3edjgnxf.js";
8
+ import"./chunk-rrnee9mk.js";
9
+ import"./chunk-v01c0k6j.js";
10
+ import"./chunk-d3ksv8y1.js";
11
+ export {
12
+ isOpaqueConnectReason2 as isOpaqueConnectReason,
13
+ opaqueTunnelRefusal2 as opaqueTunnelRefusal,
14
+ runSrtStep2 as runSrtStep,
15
+ secretFilter2 as secretFilter
16
+ };
@@ -1651,7 +1651,7 @@ var package_default = {
1651
1651
  };
1652
1652
 
1653
1653
  // apps/cli/src/scaffold.ts
1654
- var cliVersion = "0.10.71";
1654
+ var cliVersion = "0.10.73";
1655
1655
  var packageJson = (name, scaffold) => `${JSON.stringify({
1656
1656
  name,
1657
1657
  private: true,
@@ -1901,7 +1901,7 @@ var Usage = [
1901
1901
  "Every run, loop, agent, verify, constraint, secret, claim, deploy, dev and kit command takes --json."
1902
1902
  ].join(`
1903
1903
  `);
1904
- var agentRuntime = promise(() => import("./chunk-g4xsa9ks.js"));
1904
+ var agentRuntime = promise(() => import("./chunk-0rzh8yfd.js"));
1905
1905
  var TuiLauncher = Reference("kitman/cli/TuiLauncher", {
1906
1906
  defaultValue: () => gen(function* () {
1907
1907
  const tui = yield* tryPromise({
@@ -4351,9 +4351,9 @@ function claimCommand(args) {
4351
4351
  }
4352
4352
  });
4353
4353
  }
4354
- var KitWriter = map8(promise(() => import("./chunk-vs10kwdr.js")), (build) => build.writeKit);
4354
+ var KitWriter = map8(promise(() => import("./chunk-t1z7atrq.js")), (build) => build.writeKit);
4355
4355
  var KitBuilder = Reference("kitman/cli/KitBuilder", {
4356
- defaultValue: () => (directory) => flatMap3(promise(() => import("./chunk-vs10kwdr.js")), (build) => build.buildKit(directory))
4356
+ defaultValue: () => (directory) => flatMap3(promise(() => import("./chunk-t1z7atrq.js")), (build) => build.buildKit(directory))
4357
4357
  });
4358
4358
  var uploadKit = (client, workspaceId, built) => gen(function* () {
4359
4359
  yield* client.uploadKitBundle(workspaceId, built.digest, { bundle: encode2(built.bundle) });
@@ -215,10 +215,57 @@ export const startEgressProxy = (input: {
215
215
  return once(server, 'listening').then(() => server)
216
216
  }
217
217
 
218
- // `bun egress-proxy.ts <comma-separated allowlist>`
219
- if (import.meta.main)
220
- void startEgressProxy({
221
- hosts: (Bun.argv[2] ?? '').split(',').filter(host => host.length > 0),
218
+ /** Passes every connection to the proxy listening on the Unix socket `socket`, which decides what leaves. */
219
+ export const startEgressRelay = (input: {
220
+ readonly socket: string
221
+ readonly port: number
222
+ readonly hostname: string
223
+ }): Promise<Server> => {
224
+ const server = createServer(client => {
225
+ const upstream = connect(input.socket)
226
+ let connected = false
227
+
228
+ client.pipe(upstream)
229
+ upstream.pipe(client)
230
+ upstream.once('connect', () => {
231
+ connected = true
232
+ })
233
+ upstream.on('error', () => {
234
+ if (connected) client.destroy()
235
+ else
236
+ client.end(
237
+ "HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\nKitman's egress relay cannot reach the Runner's secret proxy.\n"
238
+ )
239
+ })
240
+ client.on('error', () => upstream.destroy())
241
+ }).listen(input.port, input.hostname)
242
+
243
+ return once(server, 'listening').then(() => server)
244
+ }
245
+
246
+ /** Whether a connection to the Unix socket `socket` opens. */
247
+ export const probeSocket = (socket: string): Promise<boolean> => {
248
+ const probe = connect(socket)
249
+
250
+ return once(probe, 'connect').then(
251
+ () => (probe.destroy(), true),
252
+ () => false
253
+ )
254
+ }
255
+
256
+ const main = (argv: readonly string[]) => {
257
+ const [mode = '', socket = ''] = argv
258
+
259
+ if (mode === '--probe') return probeSocket(socket).then(opened => process.exit(opened ? 0 : 1))
260
+
261
+ if (mode === '--relay') return startEgressRelay({ socket, port: 3128, hostname: '0.0.0.0' })
262
+
263
+ return startEgressProxy({
264
+ hosts: mode.split(',').filter(host => host.length > 0),
222
265
  port: 3128,
223
266
  hostname: '0.0.0.0'
224
267
  })
268
+ }
269
+
270
+ // `bun egress-proxy.ts <comma-separated allowlist>`, `bun egress-proxy.ts --relay <socket>` or `--probe <socket>`
271
+ if (import.meta.main) void main(Bun.argv.slice(2))
package/dist/index.js CHANGED
@@ -8,12 +8,12 @@ if (args[0] === "srt") {
8
8
  const { runSrtCli } = await import("./chunk-rm83dbpv.js");
9
9
  await runSrtCli();
10
10
  } else if (args[0] === "srt-step") {
11
- const { runSrtStep } = await import("./chunk-wtpwkjws.js");
11
+ const { runSrtStep } = await import("./chunk-gmnqchsc.js");
12
12
  await runSrtStep(args.slice(1));
13
13
  } else if (args[0] === "opencode-read") {
14
14
  const { runOpencodeReader } = await import("./chunk-60333x3q.js");
15
15
  runOpencodeReader();
16
16
  } else {
17
- const { runCli } = await import("./chunk-y7p921rs.js");
17
+ const { runCli } = await import("./chunk-r8rcfjag.js");
18
18
  runCli(args);
19
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitman-sh/cli",
3
- "version": "0.10.71",
3
+ "version": "0.10.73",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {
@@ -1,109 +0,0 @@
1
- // @bun
2
- import {
3
- getOrUndefined,
4
- flatMap,
5
- toArray,
6
- decodeUnknownOption,
7
- optionalKey,
8
- String4,
9
- Struct,
10
- Record,
11
- ArraySchema,
12
- decodeJsonStringOption,
13
- encodeUnknownJsonStringSync
14
- } from "./chunk-d3ksv8y1.js";
15
- import {
16
- replaceSecretReferences,
17
- secretReferencesIn
18
- } from "./chunk-rrnee9mk.js";
19
-
20
- // packages/agent-runtime/src/secret-proxy.ts
21
- import { appendFileSync, readFileSync, renameSync, writeFileSync } from "fs";
22
-
23
- // packages/agent-runtime/src/egress-proxy.ts
24
- var egressAllowed = (hosts, host, port) => {
25
- const target = host.toLowerCase();
26
- return hosts.some((entry) => {
27
- const [name = "", entryPort] = entry.toLowerCase().split(":");
28
- if (entryPort !== undefined && Number(entryPort) !== port)
29
- return false;
30
- return name.startsWith("*.") ? target.endsWith(name.slice(1)) : target === name;
31
- });
32
- };
33
- if (false)
34
- ;
35
-
36
- // packages/agent-runtime/src/secret-proxy.ts
37
- var basicPrefix = /^basic\s+/i;
38
- var basicCredential = (value) => {
39
- if (!basicPrefix.test(value))
40
- return;
41
- const decoded = Buffer.from(value.replace(basicPrefix, ""), "base64").toString("utf8");
42
- return secretReferencesIn(decoded).length === 0 ? undefined : decoded;
43
- };
44
- var referencesInHeader = (value) => [
45
- ...new Set([...secretReferencesIn(value), ...secretReferencesIn(basicCredential(value) ?? "")])
46
- ];
47
- var swapped = (value, registry) => {
48
- const swap = (reference) => registry[reference]?.value ?? reference;
49
- const basic = basicCredential(value);
50
- return basic === undefined ? replaceSecretReferences(value, swap) : `Basic ${Buffer.from(replaceSecretReferences(basic, swap)).toString("base64")}`;
51
- };
52
- var decideSecretRequest = (request, readRegistry) => {
53
- const carrying = request.headers.flatMap(([name, value]) => {
54
- const references = referencesInHeader(value);
55
- return references.length === 0 ? [] : [{ name, value, references }];
56
- });
57
- const inTarget = secretReferencesIn(request.target);
58
- if (carrying.length === 0 && inTarget.length === 0)
59
- return { action: "allow" };
60
- const registry = readRegistry();
61
- const refusals = [
62
- ...inTarget.map((reference) => ({
63
- reference,
64
- host: request.host,
65
- reason: "A reference is swapped only in a header, never in a URL."
66
- })),
67
- ...[...new Set(carrying.flatMap((header) => header.references))].flatMap((reference) => {
68
- const entry = registry?.[reference];
69
- const reason = entry === undefined ? "The reference is unknown or revoked." : request.scheme !== "https" ? "A reference is swapped only over HTTPS." : !egressAllowed(entry.hosts, request.host, request.port) ? `The Secret may be sent only to ${entry.hosts.join(", ")}.` : undefined;
70
- return reason === undefined ? [] : [{ reference, host: request.host, reason }];
71
- })
72
- ];
73
- if (refusals.length > 0)
74
- return {
75
- action: "deny",
76
- reason: `Kitman refused this request: ${refusals.map((refusal) => `${refusal.reference}: ${refusal.reason}`).join(" ")}`,
77
- refusals
78
- };
79
- const names = new Set(carrying.map((header) => header.name.toLowerCase()));
80
- return {
81
- action: "allow",
82
- setHeaders: request.headers.filter(([name]) => names.has(name.toLowerCase())).map(([name, value]) => [name, registry === undefined ? value : swapped(value, registry)])
83
- };
84
- };
85
- var Registry = Record(String4, Struct({ value: String4, hosts: ArraySchema(String4) }));
86
- var Refusal = Struct({
87
- reference: optionalKey(String4),
88
- host: String4,
89
- reason: String4
90
- });
91
- var readSecretRegistry = (file) => {
92
- try {
93
- return getOrUndefined(flatMap(decodeJsonStringOption(readFileSync(file, "utf8")), decodeUnknownOption(Registry)));
94
- } catch {
95
- return;
96
- }
97
- };
98
- var writeSecretRegistry = (file, registry) => {
99
- const next = `${file}.next`;
100
- writeFileSync(next, encodeUnknownJsonStringSync(registry), { mode: 384 });
101
- renameSync(next, file);
102
- };
103
- var secretRefusalLog = (registryFile) => `${registryFile}.refused`;
104
- var appendSecretRefusals = (registryFile, refusals) => appendFileSync(secretRefusalLog(registryFile), refusals.map((refusal) => `${encodeUnknownJsonStringSync(refusal)}
105
- `).join(""), { mode: 384 });
106
- var parseSecretRefusals = (text) => text.split(`
107
- `).flatMap((line) => toArray(flatMap(decodeJsonStringOption(line), decodeUnknownOption(Refusal))));
108
-
109
- export { decideSecretRequest, readSecretRegistry, writeSecretRegistry, secretRefusalLog, appendSecretRefusals, parseSecretRefusals };
@@ -1,100 +0,0 @@
1
- // @bun
2
- import"./chunk-d3ksv8y1.js";
3
- import"./chunk-rrnee9mk.js";
4
- import"./chunk-v01c0k6j.js";
5
- import {
6
- decideSecretRequest,
7
- readSecretRegistry,
8
- appendSecretRefusals
9
- } from "./chunk-1w81w56v.js";
10
-
11
- // packages/agent-runtime/src/srt-step.ts
12
- import { SandboxRuntimeConfigSchema } from "@anthropic-ai/sandbox-runtime";
13
- import { spawn } from "child_process";
14
- import { readFileSync } from "fs";
15
- var secretFilter = (registryFile) => (request, info) => {
16
- const url = new URL(request.url);
17
- const decision = decideSecretRequest({
18
- headers: [...request.headers.entries()],
19
- host: info?.target?.host ?? url.hostname,
20
- port: info?.target?.port ?? (url.port === "" ? url.protocol === "https:" ? 443 : 80 : Number(url.port)),
21
- target: info?.requestTarget ?? `${url.pathname}${url.search}`,
22
- scheme: info?.scheme ?? (url.protocol === "https:" ? "https" : "http")
23
- }, () => readSecretRegistry(registryFile));
24
- if (decision.action === "allow")
25
- return Promise.resolve(decision.setHeaders === undefined ? { action: "allow" } : { action: "allow", setHeaders: decision.setHeaders.map(([name, value]) => [name, value]) });
26
- try {
27
- appendSecretRefusals(registryFile, decision.refusals);
28
- } catch {}
29
- return Promise.resolve({ action: "deny", status: 403, mark: "secret-refused", reason: decision.reason });
30
- };
31
- var stepSandboxConfig = (settings, registryFile) => {
32
- const parsed = SandboxRuntimeConfigSchema.safeParse({
33
- ...settings,
34
- network: { ...settings.network, tlsTerminate: {}, refuseOpaqueTunnels: true }
35
- });
36
- return parsed.success ? {
37
- success: true,
38
- data: { ...parsed.data, network: { ...parsed.data.network, filterRequest: secretFilter(registryFile) } }
39
- } : parsed;
40
- };
41
- var opaqueTunnel = /^deny (?:socks-tunnel (\S+) \(refuseOpaqueTunnels\)|http-request CONNECT (\S+) \((?:tunnel carries no TLS|this proxy does not tunnel CONNECTs)[^)]*\))/;
42
- var refusedTunnelHost = (line) => {
43
- const match = opaqueTunnel.exec(line);
44
- return match === null ? undefined : match[1] ?? match[2];
45
- };
46
- var shellQuoted = (arg) => `'${arg.replaceAll("'", "'\\''")}'`;
47
- var argument = (args, flag) => {
48
- const at = args.indexOf(flag);
49
- return at === -1 ? undefined : args[at + 1];
50
- };
51
- var runSrtStep = async (args) => {
52
- const separator = args.indexOf("--");
53
- const settingsFile = argument(args, "--settings");
54
- const registryFile = argument(args, "--secrets");
55
- const command = separator === -1 ? [] : args.slice(separator + 1);
56
- if (settingsFile === undefined || registryFile === undefined || command.length === 0) {
57
- console.error("Usage: srt-step --settings <file> --secrets <registry> -- <command>");
58
- process.exit(2);
59
- }
60
- delete process.env.SRT_DEBUG;
61
- const { SandboxManager } = await import("@anthropic-ai/sandbox-runtime");
62
- const config = stepSandboxConfig(JSON.parse(readFileSync(settingsFile, "utf8")), registryFile);
63
- if (!config.success) {
64
- console.error(`srt-step: ${settingsFile} does not hold a valid srt config: ${config.error.message}`);
65
- process.exit(2);
66
- }
67
- await SandboxManager.initialize(config.data);
68
- const recorded = new WeakSet;
69
- SandboxManager.getSandboxViolationStore().subscribe((violations) => {
70
- const refusals = violations.flatMap((violation) => {
71
- if (recorded.has(violation))
72
- return [];
73
- recorded.add(violation);
74
- const host = refusedTunnelHost(violation.line);
75
- return host === undefined ? [] : [{ host, reason: "A tunnel Kitman cannot see into, such as SOCKS or a CONNECT without TLS, is refused." }];
76
- });
77
- if (refusals.length === 0)
78
- return;
79
- try {
80
- appendSecretRefusals(registryFile, refusals);
81
- } catch {}
82
- });
83
- const child = spawn(await SandboxManager.wrapWithSandbox(command.map(shellQuoted).join(" ")), {
84
- shell: true,
85
- stdio: "inherit"
86
- });
87
- child.on("exit", (code, signal) => {
88
- SandboxManager.cleanupAfterCommand();
89
- process.exit(signal === null ? code ?? 0 : signal === "SIGINT" || signal === "SIGTERM" ? 0 : 1);
90
- });
91
- child.on("error", (error) => {
92
- console.error(`Failed to execute command: ${error.message}`);
93
- process.exit(1);
94
- });
95
- for (const signal of ["SIGINT", "SIGTERM"])
96
- process.on(signal, () => child.kill(signal));
97
- };
98
- export {
99
- runSrtStep
100
- };
@@ -35,6 +35,14 @@ import {
35
35
  } from "./chunk-d3ksv8y1.js";
36
36
  import"./chunk-rrnee9mk.js";
37
37
  import"./chunk-v01c0k6j.js";
38
+ import {
39
+ decodeText,
40
+ mkString,
41
+ FileSystem,
42
+ Path,
43
+ withDefault,
44
+ String2
45
+ } from "./chunk-dk0ftqwg.js";
38
46
  import {
39
47
  SecureRandom,
40
48
  layerWebCrypto,
@@ -47,14 +55,6 @@ import {
47
55
  kitmanMcpServer,
48
56
  RunnerRpcProtocolVersion
49
57
  } from "./chunk-9e452fhe.js";
50
- import {
51
- decodeText,
52
- mkString,
53
- FileSystem,
54
- Path,
55
- withDefault,
56
- String2
57
- } from "./chunk-dk0ftqwg.js";
58
58
  import {
59
59
  Packument,
60
60
  tarballMatches