agent 向けスクリプトを書く
agent が実行する PowerShell スクリプトは ほぼ 通常の .ps1 ファイルです。このページは、スクリプトのソースを見るだけでは分からない罠をまとめます。
agent はスクリプトをディスクに staging してから -File で実行する
PR #230 (agent version 0.42.0+) 以降、agent は次の動作をします:
- スクリプト本体を temp の
.ps1に書き出します。Windows なら%ProgramData%\Kanade\agent-scripts\<UUID>\kanade-<UUID>.ps1、それ以外 (dev のみ) なら$TMPDIR/kanade-agent-<UUID>/kanade-<UUID>.ps1。 - その隣に launcher
.ps1を書き、UTF-8 コンソールエンコーディングを設定してから& '<your-script>' @argsでユーザースクリプトを呼びます。 powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File <launcher>を spawn します。
結果として、あなたのスクリプトでは:
- 先頭に
[CmdletBinding()]やparam(...)を 書けます。launcher の call-operator 境界が独自スコープを生み、その中ではヘッダーが有効です。 $PSCommandPathが operator のソースパスと一致することを 期待しないでください — staged 後の temp ファイルパスになります。$PSScriptRootに 書き込まないでください (次節参照)。
Pre-0.42.0 agents used powershell -Command "<body>", which parses the body as a command-line expression and rejects [CmdletBinding()] as a syntax error. If you see "Unexpected token '[CmdletBinding()]'" in stderr, the host's agent is too old — upgrade it (see agent self-update).
run_as: user のとき $PSScriptRoot は読み取り専用
run_as: user (または system_gui) のとき、子プロセスはログオン中のユーザーとして動きます — staged ファイルを書いた LocalSystem の agent ではありません。staging ディレクトリは %ProgramData% から ACL を継承していて、Users には Read & Execute は許可されるが Modify は許可されません。
つまり:
# OK from any run_as
Get-ChildItem $PSScriptRoot # list contents
Get-Content $PSScriptRoot\anything # read
# NG from run_as: user (access denied)
New-Item -Path $PSScriptRoot\out.txt
Set-Content -Path $PSScriptRoot\log.log
代わりに $env:TEMP、$env:LOCALAPPDATA、ユーザープロファイル配下の絶対パスのいずれかに書いてください。run_as: system (SYSTEM が自分の staged dir に書ける) であっても、スクリプト終了時にディレクトリは掃除されるので、隣接ファイル書き込みはいずれにせよ壊れやすいです。
実行 identity 一覧
run_as: (マニフェスト) | 子プロセスの identity | $PSScriptRoot を読める | $PSScriptRoot に書ける | admin 権限あり |
|---|---|---|---|---|
system (デフォルト) | LocalSystem | ✓ | ✓ (ただし無意味、GC される) | はい |
user | ログオン中のユーザー | ✓ | ✗ access denied | いいえ |
system_gui | LocalSystem (ユーザーセッション内) | ✓ | ✓ (ただし無意味、GC される) | はい |
system_gui is the "PsExec -i -s" pattern — admin privilege but visible in the user's desktop session (useful for GUI tools that need both elevation and an interactive window).
stdout vs Write-Host
backend の result projector はスクリプト出力として stdout を読みます。マニフェストに inventory: ブロックがあると、stdout は単一の JSON blob としてパースされます。
進捗メッセージには Write-Host を使ってください — host ストリームに行き、stdout には行かないので JSON パースを汚しません。
Write-Host "Downloading..." # → host stream (logged but ignored by projector)
Write-Output ($obj | ConvertTo-Json) # → stdout (parsed)
メッセージ出力に Write-Output は使わないこと — stdout に期待される JSON 以外が混ざると inventory パースが失敗します。
デフォルトで UTF-8
launcher はスクリプト起動前に [Console]::OutputEncoding = UTF-8 と $OutputEncoding = UTF-8 を設定するので、ホストのシステムコードページに関係なくあなたの stdout / stderr は UTF-8 になります。日本語 / DE / KR / CN を含む operator スクリプトも、ホスト個別の workaround なしに SPA Activity ビューで正しく表示されます。
明示的に OEM / CP932 / Shift-JIS 出力が必要なら ($OutputEncoding を無視する legacy CLI を呼ぶケースなど)、launcher の prelude が走ったあとにスクリプト内で自分で設定してください — あなたの代入が優先されます。
ネイティブコマンドの exit code
スクリプトが成功した native コマンドで終わると全体の exit は 0 — これは PowerShell のデフォルトです。native コマンドが失敗 ($LASTEXITCODE -ne 0) してハンドリングしないと、PowerShell は依然として 0 で終了します — $ErrorActionPreference = 'Stop' は 救ってくれません。
Windows PowerShell 5.1 (Windows エンドポイントのデフォルト、agent の
powershell.exeが解決する先) は$ErrorActionPreferenceに関わらず native コマンドの非ゼロ exit を non-terminating として扱います。PowerShell 7.3+ で$PSNativeCommandUseErrorActionPreference = $trueが追加されてこれを terminating にできますが、デプロイターゲットでは使えません。必ず$LASTEXITCODEを明示的にチェックしてください。
agent も $LASTEXITCODE を自動 propagate しません — そうしてしまうと、スクリプトが native エラーを正しく処理した場合でも非ゼロで終わってしまうからです。特定の native 呼び出しの exit code をスクリプトの exit に反映させたい場合は自分で propagate してください:
& git pull
if ($LASTEXITCODE -ne 0) { throw "git pull failed with exit code $LASTEXITCODE" }
# or, if you want the exact native code propagated:
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
通常は throw のほうが望ましい — クリーンな PowerShell エラーレコードを生成し (trap { … break } クリーンアップパターンが捕まえられる) かつ非ゼロで終わるからです。exit $LASTEXITCODE は呼び出し側が正確な exit code を気にする場合に向きます。
タイムアウト
マニフェストの timeout: は agent によって適用されます。発火すると agent は PowerShell プロセスに child.kill() を呼びます — graceful な shutdown も trap も finally もありません。それを前提に設計してください:
- スクリプトが
timeout * 0.6で終わるように実行時間を見積もり、余裕を残す。 - 明示的解放が要るリソース (staging dir、lock ファイル) のクリーンアップには
trap { ... ; break }を使う —trapは terminating エラーで発火し、agent の kill では発火しません。timeout ケースには頼らないこと。 - If you need cooperative cancellation, poll a sentinel file or a registry value and exit early. The agent has no way to send the script a graceful "wrap up" signal.
実行中ジョブの kill
kanade kill <exec_id> は agent が購読する kill メッセージを publish します。受信すると agent は child.kill() を呼びます — timeout 経路と同じ hard-kill。operator にはただちに Killed マークの result row が届き、終了前に agent がキャプチャできた stdout / stderr が付きます。