Engineering2026年5月30日|28分published

安全性はfan-inに宿る:fail-closedな並列マルチハーネス設計

エージェント基盤で複数のHarnessを並列実行しても安全性を弱めないための5つの実装規律

Engineering Case Study読解ラベル

既知の工学・数理手法をMARIA OSの実装・業種運用へ落とす記事。新理論の主張ではなく、再現可能な設計判断を重視します。

作成来歴:ARIA-TECH-01G1.U1.P9.Z1.A2
レビュー担当:ARIA-QA-01ARIA-WRITE-01

多くのチームは、安全検査を並列化した瞬間に、安全を少しだけ手放している。しかも、ほとんどの場合それに気づけない。

エージェント基盤では、1つのactionに対して多数の検査を走らせる。identityは正しいか。authorityは足りているか。tool callは許可範囲に入っているか。証跡はあるか。予算を越えないか。surface固有の契約を破っていないか。これらを順番に走らせればlatencyは足し算になる。だから並列化したくなる。

しかしfail-closedなsystemでは、素朴な並列化は安全性を静かに弱める。速くなったように見えるが、実行できなかった検査を見落とす。完了順で結果が揺れる。共有budgetを競合させる。実行時stateを読んだせいでreplayできなくなる。これは性能問題ではなく、統治の問題である。

安全性はfan-inに宿る。並列実行はlatency最適化にすぎない。
実行・信頼できないものは、最も制限的な側へ倒す。

この記事の背骨はこの2行だけである。fan-outは速くするための技術であり、安全性を生む場所ではない。安全性を決めるのは、並列に散らした結果をどう正規化し、欠けた結果をどう扱い、次のactionをどちらへ倒すかである。

もう1つ、この記事の隠れた原則を先に置いておく。決定では収束し、証跡では区別する。reject、timeout、store failure、low confidence、budget exhaustionは、decisionでは制限側へ収束する。しかしevidenceでは別々の理由として残す。fail-closedは雑な思考停止ではなく、debug可能性を残した保守的設計である。

1. なぜ多ハーネスを並列に走らせるのか

ここでいうHarnessは、1つのagent actionに対する独立した検査単位である。unit testのように関数の戻りだけを見るものではない。action envelope、actor identity、authority boundary、tool permission、evidence、budget、risk class、surface固有の契約を読み、actionを進めてよいか、制限すべきか、止めるべきか、人間へ返すべきかを判定する。

MARIA OSのようなagent governance runtimeでは、Harnessは大きく2種類に分かれる。横断Harnessはidentity、authority、trust、budget、evidence、audit traceのようにsurfaceをまたいで効く。部分HarnessはSales、Audit、Voice、Meeting、Workflow、Auto-Devなど、surface固有の失敗モードを検査する。

この分類は、そのまま並列実行の単位になる。横断Harness同士は同じaction snapshotを読む。部分Harnessも、それぞれのsurface contractに対して同じsnapshotを読む。互いの副作用を読まないなら、同一stage内で並列に走らせられる。

順次実行ならlatencyは足し算になる。

T_{seq}=\sum_i t_i$$

並列実行なら、理想的には最も遅いHarnessに近づく。

T_{parallel}=\max_i t_i + T_{fan\text{-}in}$$

この式だけを見ると、並列化は明らかに正しい。問題は、右辺の最後にあるfan-inを軽く見た瞬間に起きる。Harnessを速く走らせることと、Harnessの結果を安全に意思決定へ変換することは、別の設計問題である。

2. 中心命題:安全は正規化されたenvelope列へのfoldにある

並列Harness設計では、fan-outとfan-inを分けて考える必要がある。

fan-outは、action snapshotを複数のHarnessへ配る。ここで欲しいのは独立性、timeout、resource cap、trace ID、episode IDである。fan-inは、それらのHarness結果を1つのruntime decisionへ畳み込む。ここで欲しいのは可換性、単調性、fail-closed、監査可能性である。

ただし、fan-inへ入れるものは生のdomain resultではない。static Harness、dynamic Harness、meeting gate、audit gate、budget check、policy checkが、それぞれ別の単位、別のscore、別のseverityを返すなら、foldは横断的に安全性を保証できない。安全性は、正規化されたenvelope列に対するfoldに宿る。

正規化関数はglue codeではない。Trusted Computing Base、つまり信頼計算基盤の一部である。domain resultをenvelopeへ写すmappingが雑なら、domainのfailがenvelopeのwarnに化ける。これはサイレントな降格であり、fold以前に安全性が壊れている。

したがって正規化mappingはmonotoneでなければならない。domainでより制限的な結果は、envelopeでもより制限的なdecisionへ写る。共通score scaleも意味を持たなければならない。全Harness横断でscore 0.5が何を意味するのかを定義しない正規化は、数字を揃えただけで意味を失う。

domain固有情報は捨てない。foldが見るのは共通フィールドだけでよい。decision、score、findings、provenance、failure kindである。observedMetrics、requiredFixes、domain payloadはopaque payloadとして同乗させる。決定では収束し、証跡では区別するためである。

並列だから安全なのではない。正規化と集約が正しいから、並列にしても安全性が壊れない。

この命題を守るために、実装上の規律は5つに分かれる。

1つ目。並列の答えは、毎回同じでなければならない。2つ目。実行できなかった検査は、合格ではない。3つ目。並列にしてよいのは、独立なものだけである。4つ目。budgetを数える場所を間違えてはいけない。5つ目。再現できない並列は、監査に耐えない。

以下では、それぞれを「素朴な実装」「どう静かに壊れるか」「規律」の順で見る。

3. 規律1:並列の答えは、毎回同じでなければならない

素朴な実装はこうなる。Harnessを配列に入れ、Promise.allで実行し、戻ってきた順にreduceする。見た目は正しい。実際、すべてが成功し、すべてのHarnessが同じ速度で、集約が順序に依存しないなら問題はない。

しかし並列実行では完了順が非決定的である。network I/O、LLM call、vector store、external policy service、cache hit、GC、schedulerの都合で、同じepisodeでも戻り順は変わる。もしfoldが順序依存なら、最終判定が揺れる。

典型的な壊れ方は、最後の結果を優先する実装である。あるHarnessがreviewを返し、別のHarnessがallowを返す。完了順によって最終結果がreviewにもallowにもなる。これはrace conditionではなく、集約関数の設計ミスである。

規律は単純だ。各Harnessの生結果を共通envelopeへ正規化し、restrictivenessに全順序を置き、最も制限的なものを選ぶ。scoreは楽観側ではなく悲観側へ畳む。つまりrestrictivenessはmax、confidenceやquality scoreはminである。これなら完了順が変わっても結果は変わらない。

type Restrictiveness =
  | "allow"
  | "review"
  | "quarantine"
  | "block"

const rank: Record<Restrictiveness, number> = {
  allow: 0,
  review: 1,
  quarantine: 2,
  block: 3,
}

type HarnessDecision = {
  kind: "harness-envelope"
  harnessId: string
  decision: Restrictiveness
  score: number
  findings: string[]
  provenance: {
    source: "fulfilled" | "rejected" | "timeout" | "untrusted"
    calibrationVersion: string
  }
  payload?: unknown
}

function foldDecisions(results: HarnessDecision[]): HarnessDecision {
  return results.reduce((acc, current) => ({
    kind: "harness-envelope",
    harnessId: "fan-in",
    decision:
      rank[current.decision] > rank[acc.decision]
        ? current.decision
        : acc.decision,
    score: Math.min(acc.score, current.score),
    findings: [...acc.findings, ...current.findings],
    provenance: {
      source: "fulfilled",
      calibrationVersion: acc.provenance.calibrationVersion,
    },
  }))
}

このfoldは可換に近い。少なくともdecisionとscoreについては順序非依存である。findingsの配列順だけは実行順に依存しうるので、監査ログではharnessIdでsortするか、各findingにharnessIdstartedAtを持たせる。人間が読む順序と、機械が決める安全性を混ぜてはいけない。

重要なのは、並列化によって安全性を作ろうとしないことである。並列はただ速い。安全性は、正規化されたenvelope列と、完了順に左右されないfoldに閉じ込める。

4. 規律2:実行できなかった検査は、合格ではない

次の素朴な実装はPromise.allである。これはJavaScriptでは自然な選択に見える。しかしfail-closedなHarness runtimeでは危険である。

Promise.allは1つのPromiseがrejectした時点で全体をrejectする。すると、他のHarnessが返していたかもしれないblockquarantineを捨てる。逆にcatchで握りつぶす実装では、rejectしたHarnessが存在しなかったかのように扱われる。どちらも危険である。

Harnessが実行できない理由はいくつもある。policy storeが落ちる。vector storeがtimeoutする。schema validationが例外を投げる。外部risk serviceが429を返す。LLM evaluatorがparse不能なJSONを返す。どれも「合格」ではない。検査できなかったという事実そのものが、制限側へ倒す根拠である。

ここでrejectとtimeoutは、同じoutcomeだが違うメカニズムである。rejectはPromiseがsettleしてerrorを返す。timeoutはまだsettleしていない。だからtimeoutはallSettledだけでは扱えない。各HarnessをPromise.raceでper-harness timeoutと競争させ、timeout側を制限的なenvelopeへ変換する。

timeout値はcalibration定数である。1つの遅いHarnessが全体のbudgetを食わないように個別に置く。さらに、policy thresholdと同じようにversionとprovenanceを持たせる。なぜそのHarnessだけ800msなのか、なぜ別のHarnessは3秒なのかを、後から説明できなければならない。

規律はallSettledを使うこと。そしてrejectedtimeoutuntrusted outputを、最も制限的な結果として正規化することだ。ここで背骨の2行目が初めて実装になる。

function withTimeout<T>(
  task: Promise<T>,
  timeoutMs: number,
): Promise<T | "timeout"> {
  return Promise.race([
    task,
    new Promise<"timeout">((resolve) =>
      setTimeout(() => resolve("timeout"), timeoutMs),
    ),
  ])
}

async function evaluateAll(
  snapshot: ActionSnapshot,
  harnesses: Harness[],
): Promise<HarnessDecision> {
  const settled = await Promise.allSettled(
    harnesses.map((harness) =>
      withTimeout(
        harness.evaluate(snapshot),
        harness.calibration.timeoutMs,
      ),
    ),
  )

  const normalized = settled.map((result, index): HarnessDecision => {
    const harnessId = harnesses[index].id

    if (result.status === "fulfilled" && result.value !== "timeout" && isTrusted(result.value)) {
      return result.value
    }

    return {
      kind: "harness-envelope",
      harnessId,
      decision: "quarantine",
      score: 0,
      findings: ["harness_unavailable_or_untrusted"],
      provenance: {
        source:
          result.status === "fulfilled" && result.value === "timeout"
            ? "timeout"
            : result.status === "rejected"
              ? "rejected"
              : "untrusted",
        calibrationVersion: harnesses[index].calibration.version,
      },
    }
  })

  return foldDecisions(normalized)
}

ここではblockではなくquarantineにしている。これは設計選択である。外部actionの実行直前ならblockでよい。draft generationのように副作用がない処理ならquarantineにして、人間レビューまたは再評価へ送る方が運用しやすい。重要なのは、少なくともallowにはしないことだ。

ただし、timeoutしたHarnessは裏でまだ走り続けている可能性がある。runtimeが「閉じる」と判定した後に、遅れて完了する仕事がある。この設計が安全なのは、Harnessがsnapshotを読んでenvelopeを返すだけの純関数であり、書き込み副作用を持たないからである。もしHarnessがstoreへ書くなら、timeout後に遅れて着地する副作用が事故になる。timeoutの安全性は、後で述べるsnapshotと純粋性に依存している。

決定ではtimeoutもrejectionも制限側へ収束する。しかし証跡では区別する。timedOutHarnessIdsfailedHarnessIdsは別々に残す。timeoutは遅延、容量、外部依存の問題であり、rejectはbug、schema error、例外の問題である。outcomeを収束させても、debug情報まで潰してはいけない。

この規律は、障害時のUXを悪くするように見える。しかしagent governanceでは、検査不能のときに進めるUXの方がはるかに悪い。安全なsystemは、壊れたときに静かに楽観しない。

5. 規律3:並列にしてよいのは、独立なものだけ

3つ目の壊れ方は、依存関係の隠蔽である。横断Harnessと部分Harnessを全部まとめてfan-outする。最初は問題ない。ところがある日、authority Harnessがsurface Harnessの出力を読み始める。あるいはbudget Harnessがtool Harnessの推定costを読む。あるいはtrust Harnessがevidence Harnessのconfidenceを読む。

この瞬間、暗黙の依存辺ができる。依存があるものを同じstage内で並列実行すると、読んだり読まなかったりする。古い値を読む。空の値を読む。前回episodeの値を読む。最悪の場合、cycleができる。Harness runtimeが監査基盤であるはずなのに、監査基盤自体が非決定的になる。

規律は、依存をDAGとして宣言することだ。依存は、発見するものではなく宣言するものである。第1段では、依存宣言から実行stageを導出する。stage内は並列、stage間は順次。cycle検出はこの時点で無料で付いてくる。

第2段では、宣言した依存が読める範囲そのものを縛る。priorResultsを全結果Mapとして渡してはいけない。宣言済み依存だけを含むRecordへ絞る。これで依存宣言は順序のヒントではなく、強制力のある契約になる。

flowchart LR
  S[Action Snapshot] --> A[Identity Harness]
  S --> B[Authority Harness]
  S --> C[Evidence Harness]
  A --> D[Surface Contract Harness]
  B --> D
  C --> E[Budget Harness]
  D --> F[Fan-in Fold]
  E --> F
type HarnessNode = {
  id: string
  dependsOn: string[]
  evaluate: (input: {
    snapshot: ActionSnapshot
    priorResults: Record<string, HarnessDecision>
  }) => Promise<HarnessDecision>
}

async function runHarnessDag(
  snapshot: ActionSnapshot,
  stages: HarnessNode[][],
): Promise<HarnessDecision> {
  const decisions = new Map<string, HarnessDecision>()

  for (const stage of stages) {
    const settled = await Promise.allSettled(
      stage.map((node) =>
        node.evaluate({
          snapshot,
          priorResults: pick(decisions, node.dependsOn),
        }),
      ),
    )

    settled.forEach((result, index) => {
      const node = stage[index]
      decisions.set(node.id, normalizeSettled(node.id, result))
    })
  }

  return foldDecisions([...decisions.values()])
}

この実装では、stageそのものは事前にtopological sortされている前提である。実際のruntimeでは、起動時またはdeploy時にDAGを検証する。存在しない依存を参照していないか。cycleがないか。同一stage内で互いを参照していないか。高リスクHarnessが未承認の依存を追加していないか。

pick(decisions, node.dependsOn)が重要である。Harnessは宣言していない結果を読めない。宣言グラフが真のdataflow graphになる。これにより、依存は実装の副作用から推測するものではなく、review可能なcontractになる。

並列化の単位は分類ではなく独立性で決まる。分類は良い手がかりだが、最後に信じてよいのはDAGである。依存はコメントではなくデータにする。そうしないと、コードレビューでしか見えない安全性になる。

6. 規律4:数える場所を、間違えない

budget raceは地味だが危険である。素朴な実装では、各Harnessが共有budgetを読む。自分のactionが予算内か判断する。必要ならbudgetを消費済みに更新する。単体では正しい。しかしfan-out内でこれをやると、read-decide-writeが競合する。

例えば残りbudgetが100で、3つのHarnessがそれぞれ60のcostを見積もる。全員が同時に残り100を読む。全員が「60なら通る」と判断する。最終的に180のcostが許可される。これはDB transactionの話に見えるが、Harness runtimeではもっと広い。token、tool call、external API、customer-visible action、approval capacity、human review queueもbudgetである。

規律は、budget判定をfan-outに置かないことだ。fan-out内のHarnessはcost estimate、risk estimate、resource requestを返すだけにする。fan-in後に一度だけ、正規化された合計をbudget policyへ渡す。

この設計では、個別Harnessは予算を消費しない。budget Harnessだけが、集約済みrequestを見て判定する。必要ならreservation tokenを発行する。そのtokenがないactionは実行できない。

budgetは安全性だけでなく監査にも関係する。どのHarnessがいくら見積もり、fan-inでどう合計され、どのpolicyが許可または拒否したか。この列が残っていないと、後からcost anomalyを説明できない。

実装上のポイントは、budgetをbooleanにしないことだ。allowed: trueではなく、requestedCostreservedCostbudgetScopereservationIdexpiresAtを持つ。許可は状態ではなく、期限付きの証跡である。

7. 規律5:再現できない並列は、監査に耐えない

最後はsnapshotである。素朴な実装では、各Harnessが必要なstoreを直接読む。user state、policy state、memory、tool registry、budget ledger、evidence store、risk profileをそれぞれのタイミングで読む。これも単体では自然に見える。

しかし並列実行では、Harnessごとに読んだ世界が違うことがある。A Harnessはpolicy version 12を読み、B Harnessはpolicy version 13を読む。C Harnessはmemory write前を読み、D Harnessはwrite後を読む。あるHarnessはevidence追加前に落ち、別のHarnessは追加後に通る。このepisodeをreplayしても同じ判定にならない。

監査に耐える並列Harnessは、episode時点で凍結したsnapshotを読む純関数に近づけるべきである。すべてのHarnessが同じActionSnapshotを受け取り、そこにpolicyVersion、toolRegistryVersion、budgetLedgerVersion、evidenceRefs、memoryRefs、inputHash、createdAtを含める。

type ActionSnapshot = {
  episodeId: string
  actionId: string
  actorId: string
  mariaCoordinate: string
  inputHash: string
  policyVersion: string
  toolRegistryVersion: string
  budgetLedgerVersion: string
  evidenceRefs: string[]
  memoryRefs: string[]
  createdAt: string
}

snapshotは巨大なcopyである必要はない。多くの場合はversioned refsでよい。重要なのは、Harnessが実行中に好きなstoreを好きなタイミングで読むのではなく、episodeが固定した世界を読むことだ。

この設計にすると、Harnessは再実行できる。なぜreviewになったのかを説明できる。新しいHarnessを追加したとき、過去episodeに対してbacktestできる。人間レビューの判断とmachine decisionの差分を比較できる。

再現性は監査のためだけではない。修復のためでもある。再現できない失敗は直しにくい。直せない失敗は、学習に変換しにくい。

8. 5つは2つに畳める

ここまで5つの規律を分けて見た。しかし実際には、すべて同じ2つの原則へ畳める。

1つ目は、正規化されたenvelope列に対する順序非依存のfoldである。完了順に依存しない。stage内の実行順に依存しない。Harness数が増えても、restrictivenessは最も制限的な値へ、scoreは最も低い値へ畳む。budgetもfan-in後に一度だけ判定する。DAGも、どの結果がいつ読めるかを明示してfoldの前提を守るためにある。snapshotも、foldに入る値が同じ世界から来ることを保証するためにある。

2つ目は、実行・信頼できないものを制限側へ倒すことである。rejectしたHarnessは合格ではない。timeoutしたHarnessは合格ではない。parse不能なLLM outputは合格ではない。storeが読めなかったbudget checkは合格ではない。versionが混ざったsnapshotは合格ではない。これらはすべて、allowではなくreviewquarantineblockへ倒す。

つまり、並列マルチハーネス設計の中心は大量のHarnessをどう走らせるかではない。大量のHarnessから返ってきた不完全で非同期で失敗しうるdomain resultを、どう正規化されたenvelopeへ変換し、どう1つの保守的なdecisionへ畳むかである。

正規化が入ると拡張性も変わる。新しいdomain Harnessを足すとは、既存foldを書き換えることではない。monotoneなnormalizerを書き、共通score scaleの意味を満たし、domain payloadを保持するenvelopeを生成することになる。foldは1つのまま増やせる。

ここでカタルシスがある。5つの規律は別々のベストプラクティスではない。全部、同じ姿勢の実装である。

安全性はfan-inに宿る。並列実行はlatency最適化にすぎない。実行・信頼できないものは、最も制限的な側へ倒す。

9. 監視基盤が、自分の哲学を体現する

この設計が面白いのは、監視基盤そのものが、監視対象に要求している哲学で動く点である。

agentにはauthority boundaryを守れと言う。ではHarness runtime自身がstoreを読めないとき、どうするのか。agentには証跡なしに進むなと言う。ではHarness自身のconfidenceが低いとき、どうするのか。agentにはbudgetを越えるなと言う。ではbudget ledgerが競合したとき、どうするのか。agentには不確実なら人間へ返せと言う。ではHarnessが実行できないとき、どうするのか。

答えは全部同じでなければならない。信頼できないなら、人間へ返す。制限側へ倒す。quarantineする。blockする。少なくともallowしない。

ただし、debug情報まで同じ箱へ潰してはいけない。決定では収束し、証跡では区別する。store failure、Harness reject、timeout、low confidence、budget exhaustionは、runtime decisionでは制限側へ収束する。しかしevidenceでは別々に残す。timedOutHarnessIdsfailedHarnessIdsloopBudgetReliabilityReasonlowConfidenceFindingsは、運用上まったく別の意味を持つ。

この自己一貫性がないsystemは、時間とともに破綻する。表面上はagentに統治を要求していても、統治基盤自身が楽観的に失敗するなら、最も重要な境界はそこから漏れる。

例えばLearning Storeが一時的に読めないとする。素朴なsystemは「学習なしで進める」と判断するかもしれない。しかし過去に同じ失敗があったかもしれない。人間が同じpatchを拒否していたかもしれない。高リスクsurfaceで再発禁止になっていたかもしれない。storeが読めないことは、情報がないことではない。安全判断に必要な情報へ到達できないことである。

だから制限側へ倒す。低リスクならreviewへ、実行直前ならquarantineへ、高リスクならblockへ送る。これは保守的すぎるように見える。しかし統治できる自律では、停止や人間返却は失敗ではない。境界を守るための正常動作である。

ここで初めて、この設計が単なるHarness runtimeの実装技法ではなく、統治できる自律の一部であることが見える。agentを統治するsystemは、自分自身も同じ規律で統治されなければならない。そうでなければ、自律性は外側から見た統制の演出にすぎなくなる。

10. 速くしても壊れない、をどう設計するか

エージェント基盤を作るなら、並列化は避けられない。identity、authority、trust、evidence、budget、surface contract、quality、policy、memory、auditを毎回順番に評価していたら、runtimeは遅すぎる。latencyのためにfan-outするのは正しい。

しかし、並列は安全の設計ではない。並列はただのlatency最適化である。安全を決めるのは、fan-inである。正規化されたenvelope列に対する、完了順に依存しないfoldである。rejectやtimeoutを合格にしない姿勢である。依存をDAGとして宣言し、読めるprior resultsを縛ることである。budgetをfan-in後に一度だけ数えることである。episode snapshotでreplay可能にすることである。

この設計を守ると、Harnessを増やせる。surfaceを増やせる。agentを増やせる。新しいdomain Harnessは、foldの分岐を増やすのではなく、envelopeを生成して既存foldへcomposeする。検査を並列化しても、最終decisionは保守的に畳まれる。失敗した検査は消えず、制限側へ合成される。監査時には、同じsnapshotから同じdecisionを再構成できる。

一般化された教訓は短い。

エージェントを並列に動かすすべての人へ。並列はlatencyの最適化であって、安全の設計ではない。安全を決めるのは、常に集約と、実行できなかった時の倒し方だ。

安全性はfan-inに宿る。並列実行はlatency最適化にすぎない。

実行・信頼できないものは、最も制限的な側へ倒す。

R&D ベンチマーク

コアルール

envelope fold

並列評価そのものではなく、正規化されたenvelope列に対するfold関数に安全性を置く。

失敗の姿勢

fail-closed

reject、timeout、untrusted resultは、最も制限的な判定として合成する。

並列ユニット

DAG stage

同一stage内だけを並列化し、依存は宣言されたDAGとして扱う。

ボンギンカンにより公開され、MARIA OS編集パイプラインでレビュー済み。

© 2026 Bonginkan / MARIA OS. All rights reserved.