AIに「承認」ボタンを作った。でも、ボタンに権限は渡さなかった

連載「ChatGPTと育てる、自分専用AI基盤」Season 1 第11回。承認ボタンはOwner intentだけを送り、実行権限とlive revalidationはサーバー側に残す構造を描いた水彩画調サムネイル。 AI

※「ChatGPTと育てる、自分専用AI基盤」Season 1 第11回です。

前回は、開発のたびに私が「承認します」と返し続ける状態をやめるため、Standing Delegationを作った話を書きました。

第10回:AI開発のたびに「承認します」と打つのをやめた

安全で可逆なroutine作業は、既存のguardが通る限りAI側で続ける。本当に人間にしか決められない境界でだけ止まる。

これで承認回数はかなり減りました。でも、ゼロにはなりません。

秘密情報、production、不可逆操作、権限境界、重要なsemantic apply。そういう場所では、やはり人間の意思が必要です。

そして、その境界へ来るたびにChatGPTから長めの確認文が出て、私は内容を見て「承認します」あるいは「PR #xxx の standard merge を承認します」と返します。

前回よりずっとましです。でも、ここまでシステムを作ったなら、もう少し使いやすくできそうでした。

ボタンでよくない?

そこでChatGPT上に「承認」「拒否」「更新」を出すInteractive Control UIを作りました。

ただし、ここで一つだけ絶対にやらないと決めたことがあります。

ボタンに権限を持たせないことです。

欲しかったのは管理画面ではなく、意思決定の入力面だった

AI基盤を作っていると、つい管理画面を作りたくなります。Run一覧、Workspace一覧、PR一覧、Worker状態。大きなダッシュボードを作れば、何となく完成度が上がったようにも見えます。

でも、今回欲しかったのはそれではありませんでした。

人間が本当に判断しなければならない時に、何を承認しようとしているのか、現在どの状態なのか、拒否するのか、最新状態をもう一度見るのかを、Chatの文章より少し分かりやすく入力できればいい。

そのためInteractive Control UIは、最初から大きな管理画面ではなく小さなApproval Cardとして作りました。

最初のカードはKnowledge MCPのproduction deployだった

最初の実用対象に選んだのは、Knowledge MCPのproduction deployです。

理由は単純で、すでに境界が明確だったからです。現在versionがある。target versionがある。Gitのexact SHAがある。candidateやrollbackの考え方がある。deploy後にhealthを確認できる。

つまり「何を承認するのか」を一つのpacketとして固定しやすい。

カードには、現在version→target version、ready/blocked、main SHA、approval packet SHA、承認、拒否、更新を持たせました。

見た目としてはかなり普通の承認カードです。でも中身は、単なるボタンではありません。

ボタンを押した瞬間にdeployしてはいけない

もし実装を単純にするなら、承認ボタン→deploy()で終わります。

でも、これはやりませんでした。

カードを表示してからボタンを押すまでの間に、状態が変わる可能性があるからです。mainが進んだかもしれない。別のworkspaceが動き始めたかもしれない。Runner jobが走っているかもしれない。target versionがすでにdeploy済みかもしれない。

表示した時点では安全でも、押した時点では古いかもしれません。

第4回からずっと出てきた問題です。

「さっき見た状態」と「今から変える状態」は同じとは限らない。

だから、ボタンはdeploy命令ではなく、「この内容を承認する」というOwner intentだけを返すようにしました。

Approval Action Packetに、承認対象を閉じ込める

UI-0で作った中心がApprovalActionPacketでした。

packetには、必要に応じてaction ID、action type、target service/resource、exact target version/SHA、expected current state、risk/approval class、expiry、idempotency key、replay protection、rollback readiness、verification expectationsを持たせます。

カードの「承認」は、このpacketに結びついています。

つまり「何かを承認した」ではありません。

「このaction IDで、このtarget SHAで、この状態を前提にした、この操作を承認した」です。

状態が変われば、その承認は古くなります。

15分前のカードでも、現在状態をもう一度見る

初期実装ではpacketに15分のfreshness boundaryを持たせました。ただし15分以内なら無条件で安全という意味でもありません。

execution前にはサーバー側で、HEADとorigin/main、repository clean、active workspace、git-main lease、Runner job/mutation、target source versionなどを再確認します。

さらにKnowledge MCPの実runtime versionは、UI packetの情報を信用せず、実行直前に別経路で読み直す前提にしました。

実際、runtime revalidation toolの返り値は明示的にexecution_authorized=falseでした。

UI側の再検証が通っただけでは、まだ実行権限にはならない。これはかなり意図的です。

「UIはAuthorityではない」を仕組みにした

今回の設計を一言で言うと、こうです。

UI
  → Ownerの意思を入力する

Authority / Policy / Live State
  → その操作を今実行してよいか決める

Executor
  → 実際に実行する

UIは一番上の入力面です。安全性の本体ではありません。

ボタンの色が緑でも、server-sideでstaleなら止まる。「承認」と表示されていても、scopeが変わっていれば止まる。二回押されても、idempotency / replay protectionで同じmutationを二重実行しない。UIが壊れても、普通のChatによる承認へ戻れる。

つまり、便利なUIを追加しても、それを信頼境界にはしませんでした。

最初は、ボタンがChatGPTに出なかった

こういう話を書くと、きれいに一発で動いたように見えます。実際は、また普通に詰まりました。

Development MCPへ新しいUI toolsを追加し、runtime側では61 toolsが登録されていました。ところが、その時開いていたChatGPTのconnector schemaは古い58-tool surfaceをcacheしたままでした。

サーバーにはある。テストも通っている。runtime registrationも確認できる。でも、このChatからは新しいrendererが見えない。

こういう時、以前なら「たぶん呼べるはず」と無理に進めたくなります。でも今回は、見えていないtoolをあることにして呼びませんでした。

ボタンをfakeしない。

普通の文章承認は残っているのでfallbackで進める。connectorがrefreshされた時に本物をdogfoodする。

この判断も、Season 1で何度も繰り返してきた「現実の状態を優先する」の延長でした。

「開発を続けて」ボタンも作った

Approval Cardを作ったあと、もっと日常的に便利なボタンも欲しくなりました。

それが、「開発を続けて」です。

長い開発では、safe checkpointでChatGPTが止まり、私は次のターンで「開発を続けて」と送ることがよくあります。だったら、これもボタンでいい。

そこで開発を続けてカードを追加しました。

ここでもボタンには実行権限を持たせていません。押すとChatGPTへ固定文開発を続けて。を送るだけです。

その後、ChatGPTが現在のlive stateを読み、Standing DelegationとAuthorityの範囲で何を続けるかを判断します。

そしてこのボタンは、connector refresh後に実際にdogfoodできました。UIからChatへ開発を続けて。が入り、そのまま既存の開発フローへ戻る。

小さい機能ですが、使ってみるとかなり楽でした。

UIの裏側ではAuthorityをさらに固めていた

ちょうどこの頃、UIとは別にAuthority Layerも強くしていました。

CapabilityをA0/A1/A2/A3へ分類し、未知のCapabilityはfail closedでA3側へ倒します。

実際のdogfoodでは、A1のlocal/reversibleなsandbox.pythonはOwner interventionなしで成功しました。

一方、A3のprotected actionでは、exact target、action ID、contract SHA、Run ID、replay fingerprintまで束ねたOwner decision packetを作り、承認されない限りaction countすら増やさず止めています。

A2のbounded external actionについては、productionでは0のまま、in-memory rehearsalだけで、scope外、stale、rate超過、cost超過、prior cost evidence不足、global kill switch OFFがすべてhard denyになることを確認しました。

さらにprovider、channel、brand、project単位のexecution revocationも追加しました。

ここまでやって、Authority Layerは「AIができることを増やす層」ではなく、できることを狭める層として完成しました。

これはUIと相性がよかったです。

UIは楽にする。Authorityは緩めない。二つを同じ機能にしなかったからです。

人間を消すのではなく、人間の仕事を変えたかった

Season 1の初め、人間はかなり多くの作業をしていました。コードを運ぶ。SSHする。テストする。WordPressへ貼る。SNSへ出す。PRを確認する。「承認します」と返す。

少しずつ、その中間作業を外してきました。

最後まで残したかったのは、何を目指すか。どの境界を越えてよいか。本当に取り返しがつかないことをしてよいか。という判断です。

Interactive Control UIは、その人間の判断を消す仕組みではありません。むしろ、判断以外の摩擦を減らす仕組みでした。

「承認します」と正確な文章を打つ代わりに、内容を見てボタンを押す。ただし、押したからといって安全確認は省略されない。

この形まで来たことで、Personal AI PlatformのHuman-in-the-loopはかなり整理されました。

Season 1で最後に作ったのは、AIの機能ではなく人間との境界だった

第1回では長期記憶が欲しかった。第2回ではコードを直接触ってほしかった。第3回では強い実行権限を分離した。第4回では変更を確定する境界を作った。第5回では公開の境界を作った。第6回からは仕事の流れそのものをLoopにした。そして第9回では複数Chatまで並列化しました。

ここまでAI側を強くして、Season 1の最後に残ったテーマは、「人間をどこに残すか」でした。

Standing Delegationでroutineな往復を減らし、Authorityで実行可能範囲を機械的に狭め、UIで本当に必要な意思入力を楽にする。

これで、最初に作ろうとしていた「自分専用AI基盤」は、一度かなりまとまった形になりました。

Season 1の最終回では、Knowledge MCPから始まったこの開発が、どうしてここまで大きくなったのか。そして、当時どこを「いったん完成」と感じていたのかを振り返りました。

Season 1 Finale

第12回:記憶から並列開発まで。自分専用AI基盤は、いったんここまでできた

Season 1は第1回〜第12回で一区切りです。Season 2は別番号体系で、いったん作った基盤を「疑う」ところから続きます。

連載ナビ

コメント

タイトルとURLをコピーしました