Agent Codingのサンドボックスはどうするのがいいのか?

お仕事で長時間AI Agentを動かしたいことがほぼなかったから今まではyoloモードにあたるものは使ってなかった(プライベートでは少しつかっている)
最近ではClaude CodeにはAuto modeもあるのでallow permissionとhooksでの制限を整備すればそれほど困ることはなかった。


しかし、最近taktを使ってワークフローを組んで長時間タスクを実行するというのをやっていて、その中で裏でコマンドの承認を求められてて止まっててタイムアウトすることがまあまあ発生する。
寝てる間やっておいてもらいたいのに朝起きたら失敗していると残念な気持ちになる。
claudeの設定setting.jsonのallowリストに色々コマンドを追加していったが中々改善せず(変数とパイプでのコマンド連結駆使とかfor文使われるとかなると承認が発生する確率が上がる)
ということで、しびれを切らしてサンドボックス環境の整備に着手した。


サンドボックスといえばDockerかなと思ってちょっと最近の動向を調べていたら下記の記事辺りを見つけてなんとなくのトレンドみたいなのが分かった。

zenn.dev

blog.lai.so

Claude自体でもサンドボックスが提供されているのはうっすら知っていたがそれがmicroVMというのにあたるのとか諸々勉強になった。
Claudeのサンドボックスは馴染みがないのでそれのセットアップを今試行錯誤するのはつらいので、一旦は慣れたDocker(Devcontainer)でサンドボックスを用意してみた。 この辺りも公式ドキュメントにあるので、それを参考にちょっとカスタマイズ(これももちろんClaude Codeにやってもらう)してすぐ使えるようになったので便利である。

code.claude.com

devcontainer cliもあるのでそれでビルドや立ち上げ、execでコンテナに入れる。
まあ何かDockerfile変更したりで再ビルドすると諸々の自動化できてないセットアップを手動でやらないといけないのが面倒だけど、一旦はClaude Codeが暴れても少しは安心な環境ができたのでそちらで夜寝ている間だけ動かしつつ、昼間はallowリストの整備や他のサンドボックスの検証などをしていければなと。

久々の感覚

仕事のことで色々考えていたら、そう言えば今やってるこれって別のアレと同じような考えでいけるのでは?みたいな何かピースがうまくハマったような感覚があった。
この感覚が久々な感じがして、記憶を思い返すと20後半から30前半くらいはこの感覚がもっとあったような気がしたが、これは何か失われてしまったために少なくなったのかそれとも他にトリガーとなるようなことがあるのだろうか。

やる気ははじめから出るもんじゃなくてやってるうちに出てくるというのを実感してるという話

平日は毎朝仕事前にエニタイムで筋トレをすることが習慣化してしばらくたつが、今日はめっちゃ追い込むぞーみたいになる日なんて稀でむしろやりたくないなと思ってる。(特にスクワットの日)
でも、ジムに言ってダンベル持って何回かやってると物足りないからもう少しやるかーとやってるうちにどんどんやる気が出てきて、時間が足りないじゃないか!となって仕事に向かうみたいなことが多い。
はじめからこのやる気が出てたらなー、というのを思っていた時にちょうど下記のポストを見てまさにこれだなーと思った。

Railsアプリを型チェックできるようにSteepに入門してみた

今年も残り少なくなってきて、そういえばRailsアプリに型チェックを導入してみたいなーと思っていたのを思い出したのでSteepに入門してみた。


RailsアプリへのSteep導入は神速さんのブログがすごく丁寧だったのでそれを参考にしてやった sinsoku.hatenablog.com


差分としては

  • lib/tasks/rbs.rake を作成する
    • 現在はbin/rails g rbs_rails:installするとlib/tasks/rbs.rakeが生成される
  • 不足してるrbsを用意する
    • 自分は ActionCable::Channel::BaseActionCable::Connection::Base だけでよかった
# sig/patch.rbs
module ActionCable
  module Channel
    class Base
    end
  end

  module Connection
    class Base
    end
  end
end


あと、神速さんのブログのscaffold を試すのところにあるようにコントローラーのエラーも確かに発生していて、自分は scaffold userをしたんだけどそのエラーが下記。

# Type checking files:

..................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................F.....

app/controllers/users_controller.rb:7:4: [error] Cannot find the declaration of instance variable: `@users`
│ Diagnostic ID: Ruby::UnknownInstanceVariable
│
└     @users = User.all
      ~~~~~~

app/controllers/users_controller.rb:17:4: [error] Cannot find the declaration of instance variable: `@user`
│ Diagnostic ID: Ruby::UnknownInstanceVariable
│
└     @user = User.new
      ~~~~~

app/controllers/users_controller.rb:27:4: [error] Cannot find the declaration of instance variable: `@user`
│ Diagnostic ID: Ruby::UnknownInstanceVariable
│
└     @user = User.new(user_params)
      ~~~~~

app/controllers/users_controller.rb:31:34: [error] Type `::UsersController` does not have method `user_url`
│ Diagnostic ID: Ruby::NoMethod
│
└         format.html { redirect_to user_url(@user), notice: "User was successfully created." }
                                    ~~~~~~~~

app/controllers/users_controller.rb:45:34: [error] Type `::UsersController` does not have method `user_url`
│ Diagnostic ID: Ruby::NoMethod
│
└         format.html { redirect_to user_url(@user), notice: "User was successfully updated." }
                                    ~~~~~~~~

app/controllers/users_controller.rb:60:32: [error] Type `::UsersController` does not have method `users_url`
│ Diagnostic ID: Ruby::NoMethod
│
└       format.html { redirect_to users_url, notice: "User was successfully destroyed." }
                                  ~~~~~~~~~

app/controllers/users_controller.rb:69:4: [error] Cannot find the declaration of instance variable: `@user`
│ Diagnostic ID: Ruby::UnknownInstanceVariable
│
└     @user = User.find(params[:id])
      ~~~~~

Detected 7 problems from 1 file

path helperのやつとインスタンス変数の定義がないと怒られたので解決してみた。

path helper関連

これはrbs_rails:allをしたときにsig/rbs_rails/path_helpers.rbsが生成されているのになんでそれ見てくれないのかなと疑問だったんだけど、

# sig/rbs_rails/path_helpers.rbs

interface _RbsRailsPathHelpers
・・・
  def users_url: (*untyped) -> String
  def new_user_url: (*untyped) -> String
  def edit_user_url: (*untyped) -> String
  def user_url: (*untyped) -> String
・・・
end

このようにinterfaceで定義されているだけなので、これをコントローラーのrbsファイルでincludeしてやればよさそう。
(まだRBSが全く分かってないんだけど、RBS基礎文法最速マスター - pockestrap見つつこう使うのかなーみたいな感じでやってみたらエラーはでなくなった)

# sig/app/controllers/users_controller.rbs

class UsersController < ApplicationController
  include _RbsRailsPathHelpers

  # GET /users or /users.json
  def index: () -> untyped

  # GET /users/1 or /users/1.json
  def show: () -> nil

  # GET /users/new
  def new: () -> untyped
・・・
end

インスタンス変数

Cannot find the declaration of instance variable: `@user`
Cannot find the declaration of instance variable: `@users`

@userと@usersのRBS定義がないのでこれは手動で定義した。

# sig/app/controllers/users_controller.rbs

class UsersController < ApplicationController
  @user: User
  @users: User::ActiveRecord_Relation

  include _RbsRailsPathHelpers

  # GET /users or /users.json
  def index: () -> untyped

  # GET /users/1 or /users/1.json
  def show: () -> nil

  # GET /users/new
  def new: () -> untyped
・・・
end

あと注意点としては rbs_rails:allしたときにmodelの定義も生成してくれるけど、それはdb:migrateを実行しておかないと生成されない。

rbs_rails/rake_task.rb at 227285bd04d526525331a9b05fe9ce5e78038946 · pocke/rbs_rails · GitHub

        ::ActiveRecord::Base.descendants.each do |klass|
          next unless RbsRails::ActiveRecord.generatable?(klass)

ここの RbsRails::ActiveRecord.generatable?(klass)

rbs_rails/active_record.rb at 227285bd04d526525331a9b05fe9ce5e78038946 · pocke/rbs_rails · GitHub

    def self.generatable?(klass)
      return false if klass.abstract_class?

      klass.connection.table_exists?(klass.table_name)
    end

とあるようにDBにテーブルが存在しているかを見てるので。


これでとりあえずはsteep checkが通るようになったので、なんとか入門までは行けた感じがする。

$ bundle exec steep check
# Type checking files:

........................................................................................................................................................................................................................................

No type error detected. 🫖

ActiveJob::TestHelperでセットされるTestAdapterを無効にしたい場合

下記のように ActiveJob::TestHelperをincludeしているとqueue_adapterが強制的にTestAdapterになります。

[spec/rails_helper.rb]
RSpec.configure do |config|
  config.include ActiveJob::TestHelper
end

RSpecのJobのテストとかを書く分には便利なアサーションメソッドとかも使えて便利なのですが、本当にJobを実行したい場合もあります。 今回はActionCableのChannelでJob.perform_laterしてそのジョブで処理が完了したらbroadcastするようなものだったので、同期ではなく非同期でJobを実行したかったです。


初めは Rails.application.config.active_job.queue_adapter = :async としてqueue_adapterを切り替えようとしていたのですが、どうもTestAdapterから切り替わっていないようでした。(設定直後くらいでbinding.pryして止めても確かにAsyncAdapterにはなっている)


どうにかして部分的にAsyncAdapterに切り替えたいなと調べていると下記を見つけました。 github.com

disable_test_adapterというメソッドを呼ぶことでActiveJob::TestHelperでセットされるTestAdapterを無効にできるようです。 Rails6になった頃、config/environments/test.rbのqueue_adapter設定が無視されるのがあったんですね。

qiita.com

これでasyncアダプターに切り替えて非同期でJobを実行できるようになりました。

補足

同期実行で問題ない場合は下記で見つけた perform_enqueued_jobs を利用すると同期実行にできます。 qiita.com

RailsでPostgreSQLのVIEW定義を管理する

初めてちゃんとPostgreSQLのVIEWを使うので、Railsで使う場合の調査。


下記のgemを利用するのが定番みたい。

github.com

このgem使うと何が嬉しいのか

RailsガイドにもCREATE VIEWする方法が書かれてる。

railsguides.jp

gem利用しないとCREATE VIEWする生SQLをexecuteするだけなので

  • migrationのdownを自分で定義する必要がある
    • はじめのCREATE VIEWならdropすればいいけど以降の変更時はその前のやつに戻す必要がある
      • レビュー時に以前のmigrationを参照しないと行けないので手間が増える

マテリアライズドビューを利用するならrefreshの仕組みを用意してくれるのでさらに便利そう。


という感じかな。 あとはおまけとしてエディタでSQLシンタックスハイライトが効くので見やすいくらいですかね。

update_viewとreplace_viewはどう使い分けるのか

コメントにちゃんと書いてくれてるの助かる。

scenic/postgres.rb at 3600a485797fe1dbf30d152cc60b6d2318f04c48 · scenic-views/scenic · GitHub

CREATE OR REPLACE VIEWは、既存のビュースキーマの最後に新しいカラムを追加する場合にのみ許可されています。既存のカラムは、順番を変えたり、削除したり、型を変更することはできません [DeepLでの翻訳より]

なので、基本的にはupdate_view使う感じですね。


あとはTechRachoも目を通しておくと良さげ

techracho.bpsinc.jp