← 一覧に戻る

ダッシュボード再設計 v2: Codex指摘3点への修正設計 (j-20260503-006)

2026年5月4日 00:40 更新
MD から自動変換されたページです。内容について質問があれば右下の ? ボタンからどうぞ。

このドキュメントは j-20260503-005 の Codex adversarial review で指摘された3点の構造的欠陥に対し、修正設計を提示し、Codex に再レビューを依頼してLGTM(これで進めてよい)を取得するまで反復した記録です。実装は一切していません。

前提: 元の3指摘(再掲)

  1. 断捨離順序の自爆: refresh-dashboard.sh を1手目で削除すると complete-job.sh が依存しているため壊れる
  2. 案B「強制」が見せかけ: PreToolUse hook での enforcement は3経路で抜けられる(Bash直叩き / 100字未満手書き / allow_long=true)、ということを文書自身が認めている = enforcement theater
  3. 案C2 CHECK制約に NULL 穴: 現案だと next_action IS NULL を許容してしまい、DONE 時の不変条件になっていない

実証で確認した依存関係(指摘1の根拠固め)

refresh-dashboard.sh の呼び出し元を grep で全件抽出した結果:

つまり「完了処理」「dispatch」の二系統が依存している。Codex 指摘の通り、これを最初に消すと両系統が壊れる。

DB の verification_summary 全レコードを集計したところ:

つまり既存運用では DONE ジョブ多数が next_action NULL のまま放置されており、CHECK制約強化や migration が「素直には」通らない。クリーンアップが先行する必要がある。

Phase 2: 3指摘それぞれの修正設計

指摘1の修正案 (断捨離順序)

依存解消を1手目に置くのではなく、置換が完了してから消す という不可逆順序に並べ直す。

具体的な順序:

  1. mini-apps-ui の JobDetail / 進行中ジョブ一覧が稼働していることを確認(j-20260503-004 で完了済み、https://mbp.tail863a2a.ts.net/app/ で確認済み)
  2. complete-job.shdispatch-job.sh から refresh-dashboard.sh 呼び出しを削除する PR を出す。この時点では refresh-dashboard.sh 本体は残す(ロールバック余地)
  3. 1週間程度の運用で問題ないことを確認
  4. dashboards テーブル(pin message_id を保存している)の sweep。SELECT で残骸 chat_id を確認後 DELETE
  5. refresh-dashboard.sh 本体を削除
  6. setMessageReaction で既存 pin を unpin する処理が要るかは別途調査(残しても害は小さいが綺麗ではない)

依存グラフを逆向きに辿って消す形なので、各ステップでロールバック可能。Codex 指摘の「依存中の部品を1手目で消す」自爆は構造的に避けられる。

副次効果: dispatch-job.sh の冒頭に mini-apps-ui に DISPATCH イベントを SSE で push する処理 を入れるなら同タイミングで実装。ただし指摘1の射程外なので別ジョブ。

指摘2の修正案 (enforcement theater 解消)

3抜け道を構造的に塞ぐには、「秘書が直接 Telegram API を叩けないようにする」しかない。文面制御では塞げないというのが Codex の核心指摘なので、経路制御を取る。

修正案B-1: Bot Token を秘書セッションから隔離

現状の問題:

修正案:

副作用と論点:

修正案B-2: 完了判定をキーワード辞書に切り替え(字数依存の廃止)

現状の問題:

修正案:

副作用と論点:

修正案B-3: allow_long=true の完全廃止

現状の問題:

修正案:

「enforcement theater にならない」設計原則

修正設計から抽出した4つの原則を明文化する:

指摘3の修正案 (CHECK制約 NULL 穴)

NULL を許す書き方では DONE 時の不変条件にならない。代わりに「DONE 遷移時に強制検証する仕組み」を二重で持つ。

修正案C2-A: BEFORE UPDATE トリガーで DONE 時の next_action 非空を強制

CREATE TRIGGER enforce_next_action_on_done
BEFORE UPDATE OF status ON jobs
FOR EACH ROW
WHEN NEW.status = 'DONE' AND OLD.status != 'DONE'
BEGIN
  SELECT CASE
    WHEN (SELECT next_action FROM verification_summary WHERE job_id = NEW.id) IS NULL
      OR (SELECT json_array_length(next_action) FROM verification_summary WHERE job_id = NEW.id) = 0
    THEN RAISE(ABORT, 'DONE transition requires non-empty next_action')
  END;
END;

特性:

修正案C2-B: complete-job.sh が next_action を必須引数化

complete-job.sh JOB_ID --next-action='["next step 1", "next step 2"]'

特性:

修正案C2-C: 二重防御 (A + B 同時適用)

これが推奨。秘書がどの経路で UPDATE しようと、トリガーで止まる。スクリプト経由なら引数チェックでも止まる。同じ条件を2層で守ることで、どちらかをすり抜けても他方が捕まえる。

next_action_kind='none' の扱い(指摘の余地への回答)

業務上「本当に次がない」DONE があるか:

既存データのクリーンアップ

migration 適用前に既存 DONE ジョブ(NULL 169件 + 空配列 4件)をクリーンアップする必要がある:

ただし Codex の指摘「制約を通すために偽の next_action を埋める」破壊半径が大きいので、この修正は migration 前に「既存 DONE はトリガーから除外」する条件を入れる手もある:

WHEN NEW.status = 'DONE' AND OLD.status != 'DONE' AND NEW.id NOT IN (SELECT id FROM jobs WHERE created_at < '2026-05-03T00:00:00Z')

→ これも汚いので、クリーンアップを先にやる方を選ぶ。

Phase 3: Codex 再レビュー結果(LGTM 取得まで反復)

反復1回目: Codex(adversarial・フラットモード) 回答全文

1. 指摘1の修正案 — 結論: ギャップあり。

2. 指摘2 B-1 — 結論: ギャップあり。かなり大きい。

3. 指摘2 B-2 — 結論: 破綻している。

4. 指摘2 B-3 — 結論: ギャップあり。

5. 指摘3 C2-C — 結論: まだ閉じていない。

6. 4つの原則 — 不十分。

欠けている原則:

7. 総合判定: 修正必要

LGTM になるために直すべき点:

反復2回目: 修正設計v3 (Codex反復1の8指摘を全て反映)

指摘1 修正v3: dashboards 廃止を「段階的封じ込め」に置き換え

「ロールバック可能」表記を撤廃し、論理 archive + バックアップ + transaction で監査可能化する。

事前準備:

archive 手順(transaction + row-count gate):

BEGIN IMMEDIATE TRANSACTION;
ALTER TABLE dashboards ADD COLUMN archived_at TEXT;
SELECT 'pre', COUNT(*) FROM dashboards WHERE archived_at IS NULL;
UPDATE dashboards SET archived_at = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE archived_at IS NULL;
SELECT 'post', COUNT(*) FROM dashboards WHERE archived_at IS NULL;  -- 0 を期待
-- 期待値と異なれば ROLLBACK 手動実行
COMMIT;

成功合否条件(期間ではなく数値で):

ロールバック: cp jobs.db.bak-... jobs.db で SQLite ファイル復元 + dispatch-job.sh / complete-job.sh の git revert。pin 自体は archive 段階で touch しないので追跡可能。

指摘2 修正v3: B-1 OS principal 分離

ステップ1(短期):

ステップ2(中期、別ジョブ起票):

「秘書が送信 script を自由実行できない」要求への回答:

/app/api/internal/send-report 認証仕様v3

固定 schema 限定:

type SendReportPayload = {
  job_id: string;
  chat_id: string;
  type: 'done' | 'propose' | 'question';
  timestamp: number;  // Unix epoch sec
  nonce: string;      // UUIDv4
};

raw text フィールドを置かない。schema を満たさないリクエストは 400 で拒否。

認証:

replay 防止:

異常系:

B-2 完全廃止

keyword dictionary 案は削除。代わりに「秘書が mcp__plugin_telegram_telegram__replysend_message を一切使わない」を hook で強制:

B-3 修正v3: send-ack endpoint 新設

/app/api/internal/send-ack:

type SendAckPayload = {
  job_id: string;
  chat_id: string;
  ack_kind: 'received' | 'reading' | 'deferred' | 'noted';
  timestamp: number;
  nonce: string;
};

設計原則7箇条(Codex 指摘の3原則を統合)

  1. 経路を塞げない封鎖は封鎖と呼ばない(文面チェックは封鎖ではない)
  2. バイパス用フラグは1つでも残せばゼロにならない
  3. 共有秘密はプロセス環境変数まで降ろす + ファイルに残す場合は OS principal で分離
  4. block 時メッセージは正規経路へ誘導必須
  5. (新)信頼境界は OS 上の実体で分離する
  6. (新)特権 endpoint は caller identity + request freshness(timestamp/nonce)を検証する
  7. (新)不変条件は全 mutation path で守る

指摘3 修正v3: トリガー三段構え + schema 分離

C2-D: トリガー三段構え
-- (1) jobs.status DONE 遷移時
CREATE TRIGGER enforce_next_action_on_done_jobs
BEFORE UPDATE OF status ON jobs
FOR EACH ROW
WHEN NEW.status = 'DONE' AND OLD.status != 'DONE'
BEGIN
  SELECT CASE
    WHEN NOT EXISTS (
      SELECT 1 FROM verification_summary
      WHERE job_id = NEW.id
        AND legacy_no_next_action != 1
        AND (
          (next_action_kind = 'none'
           AND next_action_explanation IS NOT NULL
           AND TRIM(next_action_explanation) != '')
          OR (next_action IS NOT NULL
              AND json_array_length(next_action) > 0
              AND NOT EXISTS (SELECT 1 FROM json_each(next_action) WHERE TRIM(value) = ''))
        )
    )
    THEN RAISE(ABORT, 'DONE requires non-empty next_action OR next_action_kind=none with explanation')
  END;
END;

-- (2) verification_summary 側 UPDATE で DONE ジョブの next_action を空白化させない
CREATE TRIGGER enforce_next_action_on_vs_update
BEFORE UPDATE ON verification_summary
FOR EACH ROW
WHEN (SELECT status FROM jobs WHERE id = NEW.job_id) = 'DONE'
  AND NEW.legacy_no_next_action != 1
BEGIN
  SELECT CASE
    WHEN (NEW.next_action_kind = 'none'
          AND (NEW.next_action_explanation IS NULL OR TRIM(NEW.next_action_explanation) = ''))
      OR (NEW.next_action_kind != 'none'
          AND (NEW.next_action IS NULL
               OR json_array_length(NEW.next_action) = 0
               OR EXISTS (SELECT 1 FROM json_each(NEW.next_action) WHERE TRIM(value) = '')))
    THEN RAISE(ABORT, 'cannot empty/blank next_action of DONE job')
  END;
END;

-- (3) jobs INSERT 直で status='DONE' を禁止
CREATE TRIGGER forbid_direct_done_insert
BEFORE INSERT ON jobs
FOR EACH ROW
WHEN NEW.status = 'DONE'
BEGIN
  SELECT RAISE(ABORT, 'INSERT with status=DONE forbidden; use PENDING then UPDATE');
END;

これで mutation path 全域が守られる:

INSERT 順序問題は trigger (3) で解決: 新規ジョブは必ず PENDING で先に入れ → verification_summary 記録 → 最後に jobs.status を DONE へ UPDATE する順序が確定。

next_action_kind='none' の schema 分離

説明文1要素配列に潰す案を撤回し、schema で型を分離:

これで「次がない(説明あり)」と「1件以上の次アクションあり」を schema 上で型分離できる。下流処理は next_action_kind で分岐すればよい。

既存データ cleanup を非破壊化

偽 next_action 注入と updated_at 上書きを廃止:

これで「過去データの意味」を保持しつつ、新規 DONE のみに不変条件を適用できる。

反復2回目: Codex 回答全文

1. PARTIAL

2. OPEN

3. PARTIAL

4. CLOSED

5. OPEN

6. OPEN

7. CLOSED

8. CLOSED

新規ギャップ(閉じる作業で生まれた穴)

設計原則の欠落

  1. 送信本文の入力源(DB 行・template)にも write authorization と audit trail を要求する
  2. enforcement component が不在なら fail closed

総合判定: 修正必要

LGTM になるために直すべき点:

サマリー: CLOSED 3 / PARTIAL 2 / OPEN 3。次の v4 で上記5点を閉じる。

反復3回目: 修正設計v4 (Codex反復2の5点を全て閉じる)

指摘1 v4: dashboards 廃止を完全 fail-closed 化

並行書き込み停止条件と SQL の fail-closed 化を強化:

-- 事前: write fence(dashboards テーブルへの新規 write を全部 abort させる)
CREATE TRIGGER fence_dashboards_writes_pre_archive
BEFORE INSERT ON dashboards
BEGIN SELECT RAISE(ABORT, 'dashboards is being archived; new writes blocked'); END;

CREATE TRIGGER fence_dashboards_updates_pre_archive
BEFORE UPDATE ON dashboards
WHEN OLD.archived_at IS NULL
BEGIN SELECT RAISE(ABORT, 'dashboards is being archived; updates blocked'); END;

-- archive 本体
BEGIN IMMEDIATE TRANSACTION;
ALTER TABLE dashboards ADD COLUMN archived_at TEXT;
UPDATE dashboards SET archived_at = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE archived_at IS NULL;
-- post-archive count を SQL 内で検証(失敗時は SQLite が自動 ROLLBACK)
SELECT CASE
  WHEN (SELECT COUNT(*) FROM dashboards WHERE archived_at IS NULL) != 0
  THEN RAISE(ABORT, 'archive incomplete: rows still unarchived')
END;
COMMIT;

合否条件を3つから5つに拡張(他 writer 停止を含む):

  1. mini-apps-api /app/api/jobs access log が直近24時間で 100 リクエスト以上
  2. mini-apps-api 側のエラーレートが 1% 未満
  3. dispatch-job.sh / complete-job.sh の実行ログ(/tmp/refresh-dashboard.log)に新規エラー 0 件
  4. (新)dashboards テーブルへの新規 write が72時間連続 0 件(SELECT MAX(updated_at) FROM dashboards の単調検証で確認)
  5. (新)grep -rn 'FROM dashboards\|INTO dashboards\|UPDATE dashboards' ~/ の依存先一覧が72時間変化なし(新規 writer 出現の防止)

これら5条件を72時間連続で満たした上で archive を実行。

指摘2 v4: 送信 script を user 空間から完全に外す(outbox アーキテクチャ)

report-*-v3.sh廃止 する。秘書セッションは Telegram 送信のための script を一切持たない。代わりに DB 経由のトリガーで送信される outbox パターンを採用:

Bot Token 隔離(専用 principal)

将来対応に逃がさず、ステップ1で専用ユーザーを使う:

MCP プラグインの Token も同隔離

指摘3 v4: nonce の永続化と認証主体の明確化

/send-report /send-ack 内部 endpoint は outbox アーキで不要になる ため廃止。HMAC 関連の矛盾は根本から消える。

仮に外部からの送信 trigger 経路を残す場合(将来 ext system 連携など)の補強も明記:

指摘5 v4: hook 破損時の hard fallback

secretary-reply-deny-all.sh が壊れた/skip された場合の安全策:

指摘6 v4: trigger 強化(BEFORE INSERT + legacy_flag 後付け禁止)

-- (4) verification_summary BEFORE INSERT で DONE ジョブの next_action 検証
CREATE TRIGGER enforce_next_action_on_vs_insert
BEFORE INSERT ON verification_summary
FOR EACH ROW
WHEN (SELECT status FROM jobs WHERE id = NEW.job_id) = 'DONE'
  AND COALESCE(NEW.legacy_no_next_action, 0) != 1
BEGIN
  SELECT CASE
    WHEN (NEW.next_action_kind = 'none'
          AND (NEW.next_action_explanation IS NULL OR TRIM(NEW.next_action_explanation) = ''))
      OR (COALESCE(NEW.next_action_kind, '') != 'none'
          AND (NEW.next_action IS NULL
               OR json_array_length(NEW.next_action) = 0
               OR EXISTS (SELECT 1 FROM json_each(NEW.next_action) WHERE TRIM(value) = '')))
    THEN RAISE(ABORT, 'verification_summary INSERT for DONE job requires next_action or kind=none with explanation')
  END;
END;

-- (5) legacy_no_next_action 後付け禁止
CREATE TRIGGER forbid_legacy_flag_setting
BEFORE UPDATE OF legacy_no_next_action ON verification_summary
FOR EACH ROW
WHEN COALESCE(OLD.legacy_no_next_action, 0) = 0 AND NEW.legacy_no_next_action = 1
BEGIN
  SELECT RAISE(ABORT, 'legacy_no_next_action cannot be set post-hoc; only initial migration allows this');
END;

-- (6) 新規 INSERT で legacy_no_next_action=1 を仕込むのも禁止
CREATE TRIGGER forbid_legacy_flag_on_new_insert
BEFORE INSERT ON verification_summary
FOR EACH ROW
WHEN NEW.legacy_no_next_action = 1
  AND (SELECT created_at FROM jobs WHERE id = NEW.job_id) > '2026-05-04T00:00:00Z'
BEGIN
  SELECT RAISE(ABORT, 'legacy_no_next_action=1 only allowed for jobs created before migration cutoff');
END;

migration 実行時のバイパス機構:

CREATE TABLE _migration_lock (
  id INTEGER PRIMARY KEY CHECK(id=1),
  active INTEGER NOT NULL DEFAULT 0,
  released_at TEXT
);
INSERT INTO _migration_lock(id, active) VALUES (1, 1);

-- 一度 active=0 になったら二度と active=1 にできない
CREATE TRIGGER lock_migration_one_way
BEFORE UPDATE OF active ON _migration_lock
WHEN OLD.active = 0 AND NEW.active = 1
BEGIN SELECT RAISE(ABORT, 'migration lock is one-way; cannot reactivate'); END;

migration 終了後は _migration_lock.active=0 を二度と立てられないので、attacker が後で「migration 中だ」と偽装して trigger を skip することは不可能。

設計原則 9箇条(Codex 反復2 の2原則を統合)

  1. 経路を塞げない封鎖は封鎖と呼ばない
  2. バイパス用フラグは1つでも残せばゼロにならない
  3. 共有秘密はプロセス環境変数まで降ろす + ファイルに残す場合は OS principal で分離
  4. block 時メッセージは正規経路へ誘導必須
  5. 信頼境界は OS 上の実体で分離する
  6. 特権 endpoint は caller identity + request freshness を検証する
  7. 不変条件は全 mutation path で守る
  8. (新)送信本文の入力源(DB 行・template)にも write authorization と audit trail を要求する
  9. (新)enforcement component が不在なら fail closed

原則8 (DB-template body の write authorization + audit trail) の具体化

原則9 (enforcement fail-closed) の具体化

反復3回目: Codex 回答全文

Issue 1: OPENdashboards の write fence が ALTER TABLE 前に OLD.archived_at を参照しつつ、直後の archive UPDATE 自体も止める書き方になっているので、手順が文書どおりには成立しない。

Issue 2: PARTIALreport-*-v3.sh 廃止と _secretarybot 導入は入ったが、telegram_outbox 直挿し防止は「internal flag テーブルを使う」とあるだけで、そのフラグ自体を秘書が立てられない保証が仕様にない。

Issue 3: CLOSED/send-report/send-ack を live path から外したので、認証主体の矛盾は主経路から消えた。

Issue 5: OPEN。canonical path rejection はまだ実装条件に落ちておらず、「watchdog は hack」「正攻法は将来 MCP プラグイン修正」と逃がしているので、hard fallback は仕様として閉じていない。

Issue 6: PARTIALverification_summaryBEFORE INSERTlegacy_no_next_action 後付け禁止は入ったが、migration bypass は _migration_lock.activeUPDATE 0→1 だけしか塞いでおらず、DELETE や再 INSERT の封鎖が仕様にない。

outbox architecture 攻撃面:

_secretarybot user 攻撃面:

trigger hardening 攻撃面:

欠けている原則: 「外部副作用を起こす承認状態と、その監査証跡は、本文を書ける主体と同じ trust domain に置かない」

総合判定: 修正必要

サマリー CLOSED 4 / PARTIAL 2 / OPEN 2

LGTM 反転の最低条件:

  1. dashboards archive を実行可能 SQL に書き直す。fence は archive 本体を止めない形にし、DELETE も封じ、fail-closed を SQL 単体で成立
  2. telegram_outbox 直挿し防止を OS 権限設計で。秘書では enqueue 不可、worker のみ enqueue/sent 更新可能。別 DB/別プロセス境界に切り出す
  3. DONE で送信が走るなら、遷移に追加の認可条件。本文編集権限と外部送信確定権限を同一 UPDATE で兼ねさせるな
  4. _migration_lock に INSERT/DELETE 禁止を足す。trigger 本文に bypass 条件を埋め込んだ完成形 SQL
  5. jobs_audit を可変表のままにしない。DELETE/UPDATE 禁止 trigger + _secretarybot 所有別 DB への append-only 複製
  6. SQLite ファイルの offline 差し替え、WAL sidecar 権限、worker restart 権限を運用仕様として明文化

反復4回目: 修正設計v5 (Codex反復3の6点を全て閉じる + 原則10追加)

指摘1 v5: dashboards archive の実行可能 SQL

write fence は archive 完了後 に永続化する形に変える(archive 本体を止めない順序):

-- ステップ1: バックアップ(SQLite ファイル丸ごと)
-- $ cp jobs.db jobs.db.bak-$(date +%Y%m%d-%H%M%S)

-- ステップ2: archive 本体(fence は最後に張る)
BEGIN IMMEDIATE TRANSACTION;
ALTER TABLE dashboards ADD COLUMN archived_at TEXT;
UPDATE dashboards SET archived_at = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE archived_at IS NULL;

-- ステップ3: post-archive count を SQL 内で fail-closed 検証
-- (RAISE(ABORT) は transaction 全体を rollback する)
SELECT CASE
  WHEN (SELECT COUNT(*) FROM dashboards WHERE archived_at IS NULL) != 0
  THEN RAISE(ABORT, 'archive incomplete: rows still unarchived, transaction will rollback')
END;

-- ステップ4: archive 完了後に永続 fence(全 mutation 禁止)を張る
CREATE TRIGGER fence_dashboards_insert
BEFORE INSERT ON dashboards
BEGIN SELECT RAISE(ABORT, 'dashboards is archived; new writes forbidden'); END;

CREATE TRIGGER fence_dashboards_update
BEFORE UPDATE ON dashboards
BEGIN SELECT RAISE(ABORT, 'dashboards is archived; updates forbidden'); END;

CREATE TRIGGER fence_dashboards_delete
BEFORE DELETE ON dashboards
BEGIN SELECT RAISE(ABORT, 'dashboards is archived; deletes forbidden (use undo migration)'); END;

COMMIT;

順序が重要: ALTER → UPDATE → 検証 → fence 設置 → COMMIT。fence を archive に張ることで、archive 本体が止まらない。検証失敗時は RAISE で transaction 全体が rollback され、ALTER も UPDATE も巻き戻る。fence は archive 完了後の write を全 mutation(INSERT/UPDATE/DELETE)で塞ぐ。

合否条件は反復3 v4 から変更なし(5項目72時間連続)。

指摘2 v5: outbox を別 DB / 別 OS principal に物理分離

telegram_outboxjobs.db から外して、独立した outbox.db に切り出す:

しかし秘書が DONE にした時点で outbox に enqueue する必要がある。これを「同一 transaction 内 trigger」で実現するのは無理(別 DB だから)。代替設計:

5秒ポーリング遅延の許容仕様
DB 破損時の復旧

指摘3 v5: 認可の分離 (本文編集権限と送信確定権限を分離)

「秘書が DONE にしたら全部自動送信」は健人の自動化要望と一致するため維持。ただし「同一 UPDATE で兼ねさせるな」要求への回答として:

これで Codex 指摘の「同一 UPDATE で兼ねさせるな」を別 trust domain で実装している。秘書が pending_dispatches に偽の hash を仕込んでも、worker が現在 DB 値と再計算 hash を比較して reject。

指摘4 v5: _migration_lock の完成形 SQL (INSERT/DELETE 封鎖込み)

CREATE TABLE _migration_lock (
  id INTEGER PRIMARY KEY CHECK(id=1),
  active INTEGER NOT NULL DEFAULT 1,
  released_at TEXT
);

-- 初期化(migration プロセスの最初の SQL)
INSERT INTO _migration_lock(id, active) VALUES (1, 1);

-- INSERT を1回限りに制限(2回目以降の INSERT を全て abort)
CREATE TRIGGER forbid_migration_lock_reinsert
BEFORE INSERT ON _migration_lock
WHEN EXISTS (SELECT 1 FROM _migration_lock)
BEGIN SELECT RAISE(ABORT, 'migration lock already initialized; cannot re-insert'); END;

-- DELETE を全面禁止
CREATE TRIGGER forbid_migration_lock_delete
BEFORE DELETE ON _migration_lock
BEGIN SELECT RAISE(ABORT, 'migration lock cannot be deleted'); END;

-- UPDATE で active 0→1 を禁止(片道切符)
CREATE TRIGGER lock_migration_one_way
BEFORE UPDATE OF active ON _migration_lock
WHEN OLD.active = 0 AND NEW.active = 1
BEGIN SELECT RAISE(ABORT, 'migration lock is one-way; cannot reactivate'); END;

-- migration 終了処理(active=0 へ)
UPDATE _migration_lock SET active = 0, released_at = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE id = 1;

これで attacker は:

migration の trigger bypass:

CREATE TRIGGER enforce_next_action_on_done_jobs
BEFORE UPDATE OF status ON jobs
FOR EACH ROW
WHEN NEW.status = 'DONE' AND OLD.status != 'DONE'
  AND NOT EXISTS (SELECT 1 FROM _migration_lock WHERE active = 1)
BEGIN
  -- ... 既存の検証ロジック
END;

migration 中(active=1)はトリガー全体が WHEN 句で skip。終了後(active=0)は通常運用、二度と active=1 にできないので bypass 不可能。

指摘5 v5: hook 破損時の hard fallback (canonical rejection を仕様化)

3層防御を明示的に書く:

第1層: OS 権限隔離(指摘2 v5 で実現)

第2層: outbox 経由でのみ Telegram 送信が成立

📝 質問モード — テキストを選択してね
✓ 質問を送信しました