自動化ワーカーを安全に動かすために、作業ディレクトリを分離するのは自然です。ところが、タスク契約が「リポジトリ上のファイルを列挙して判断する」ことを前提にしていると、中立サンドボックスは失敗の発生源になります。
手元の失敗台帳では、全 2679 件のうち fail_type=blocked_by_input が 1208 件、fail_type=missing_inputs が 20 件ありました。根拠は次のような行です。
{"status":"partial","fail_type":"blocked_by_input","dept":"marketing","lane":"ops","project":"revenue2"}
{"status":"blocked","fail_type":"missing_inputs","dept":"video","lane":"youtube-en","project":"sci-shorts"}
この数字だけで「全部が同じ原因」とは言えません。ただし、カードが「リポジトリを見て対象を選べ」と要求しているなら、入力不足を個別事故として扱うより、実行契約を疑ったほうが再発防止に近づきます。
なお、既公開一覧の Qiita セクションには、入力検証、allow-list、ステージング失敗の記事がありました。本稿は既公開 33 本以上の Qiita 記事と照合し、重複しない「cwd 隔離とタスク契約の不整合」に絞っています。
失敗の形
最小化すると、失敗はこうです。
タスク契約:
repo/articles 配下を列挙し、未投稿の記事候補を選ぶ
実行環境:
worker の cwd は空の一時ディレクトリ
repo 本体は見えない
結果:
ファイルが存在しないので候補 0 件
ワーカーは missing_inputs または blocked_by_input で止まる
ワーカーは悪くありません。契約に必要な入力が、ワーカーから見える場所にないだけです。問題は「cwd を隔離する」設計と「cwd からリポジトリを探索する」設計が、同時に成立しないことです。
最小再現
次の Node.js スクリプトは、articles ディレクトリを列挙して最初の Markdown を選ぶだけです。
// worker.js
const fs = require("node:fs");
const path = require("node:path");
const root = process.cwd();
const articlesDir = path.join(root, "articles");
if (!fs.existsSync(articlesDir)) {
console.error(JSON.stringify({
status: "blocked",
fail_type: "missing_inputs",
reason: "articles directory is not visible from cwd"
}));
process.exit(2);
}
const files = fs.readdirSync(articlesDir)
.filter((name) => name.endsWith(".md"))
.sort();
if (files.length === 0) {
console.error(JSON.stringify({
status: "blocked",
fail_type: "blocked_by_input",
reason: "no markdown candidates"
}));
process.exit(2);
}
console.log(JSON.stringify({
status: "done",
selected: path.join("articles", files[0])
}));
リポジトリ直下で実行すると通ります。
New-Item -ItemType Directory -Force articles | Out-Null
"# hello" | Set-Content articles\a.md -Encoding UTF8
node worker.js
一方、ワーカーだけを別 cwd で起動すると落ちます。
New-Item -ItemType Directory -Force sandbox | Out-Null
Copy-Item worker.js sandbox\worker.js
Push-Location sandbox
node worker.js
Pop-Location
出るのは、おおむね次の種類のエラーです。
{"status":"blocked","fail_type":"missing_inputs","reason":"articles directory is not visible from cwd"}
これは「ファイルがない」ではなく、「契約に必要なファイルをワーカーに渡していない」です。
直し方: 探索させず、入力目録を渡す
修正は、ワーカーにリポジトリを探させないことです。呼び出し側が、許可された入力だけを manifest として渡します。
{
"inputs": [
{ "id": "a", "path": "staged/articles/a.md" },
{ "id": "b", "path": "staged/articles/b.md" }
]
}
ワーカー側はこうします。
// worker-fixed.js
const fs = require("node:fs");
const manifestPath = process.argv[2];
if (!manifestPath) {
throw new Error("usage: node worker-fixed.js manifest.json");
}
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
const candidates = manifest.inputs
.filter((item) => item.path.endsWith(".md"))
.sort((a, b) => a.path.localeCompare(b.path));
if (candidates.length === 0) {
console.error(JSON.stringify({
status: "blocked",
fail_type: "missing_inputs",
reason: "manifest has no markdown candidates"
}));
process.exit(2);
}
console.log(JSON.stringify({
status: "done",
selected: candidates[0].id
}));
実行手順です。
@'
{
"inputs": [
{ "id": "a", "path": "staged/articles/a.md" }
]
}
'@ | Set-Content sandbox\manifest.json -Encoding UTF8
Copy-Item worker-fixed.js sandbox\worker-fixed.js
Push-Location sandbox
node worker-fixed.js manifest.json
Pop-Location
この形にすると、cwd がどこであっても、ワーカーの判断材料は manifest に固定されます。
契約を2つに分ける
実装上は、次の 2 段に分けると事故が減ります。
prepare:
候補を列挙し、許可されたファイルだけを staging に置く
manifest.json を作る
worker:
staging と manifest.json だけを見る
成果物と result.json だけを書く
prepare は探索してよいが、worker は探索しない。worker の安全境界を狭くしたいなら、その内側に必要な入力をすべて置く必要があります。
blocked_by_input が多いパイプラインでは、ワーカーを責める前に「判断に必要なものが見えているか」を確認したほうがよいです。中立サンドボックスは便利ですが、ファイル列挙まで中に押し込むと、入力不足を量産します。探索は prepare、実行は worker。この分離だけで、失敗の扱いは読みやすくなります。