EN | JA

agent 向けスクリプトを書く

agent が実行する PowerShell スクリプトは ほぼ 通常の .ps1 ファイルです。このページは、スクリプトのソースを見るだけでは分からない罠をまとめます。

agent はスクリプトをディスクに staging してから -File で実行する

PR #230 (agent version 0.42.0+) 以降、agent は次の動作をします:

  1. スクリプト本体を temp の .ps1 に書き出します。Windows なら %ProgramData%\Kanade\agent-scripts\<UUID>\kanade-<UUID>.ps1、それ以外 (dev のみ) なら $TMPDIR/kanade-agent-<UUID>/kanade-<UUID>.ps1。
  2. Writes a launcher .ps1 next to it that sets UTF-8 console encoding then & '<your-script>' @args, and re-raises your script's exit N / terminating error as the job's exit code (if (-not $?) { exit $LASTEXITCODE }).
  3. powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File <launcher> を spawn します。

結果として、あなたのスクリプトでは:

  • 先頭に [CmdletBinding()] や param(...) を 書けます。launcher の call-operator 境界が独自スコープを生み、その中ではヘッダーが有効です。
  • $PSCommandPath が operator のソースパスと一致することを 期待しないでください — staged 後の temp ファイルパスになります。
  • $PSScriptRoot に 書き込まないでください (次節参照)。

0.42.0 より前のエージェントは powershell -Command "<body>" を使っており、これは本文をコマンドライン式として解析するため [CmdletBinding()] を構文エラーとして弾きます。stderr に「Unexpected token '[CmdletBinding()]'」が出ていたら、その端末のエージェントが古すぎます。更新してください (エージェントの自己更新 を参照)。

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_guiLocalSystem (ユーザーセッション内)✓✓ (ただし無意味、GC される)はい

system_gui は「PsExec の -i -s」パターンです。管理者権限を持ちつつ、ユーザーのデスクトップセッションから見える形で動きます (昇格と対話ウィンドウの両方を必要とする GUI ツールに有用です)。

stdout vs Write-Host

The backend's result projector reads stdout as the script's output. For jobs with hint blocks (inventory:, check:, collect:, etc.), the projector extracts JSON payloads delimited by #KANADE-<KIND>-BEGIN and #KANADE-<KIND>-END fences (e.g. #KANADE-INVENTORY-BEGIN / #KANADE-INVENTORY-END).

For single-hint jobs without fences, backward compatibility allows parsing the entire stdout as a single JSON object. However, for multi-hint jobs (e.g. combining inventory: and check:), fenced blocks are required so each hint handler can extract its respective payload.

Do NOT use Write-Host for progress chatter. Write-Host output bleeds INTO the captured stdout stream, which can mess up unfenced JSON parsing or clutter log output.

Send progress chatter to stderr via [Console]::Error.WriteLine(...). stderr is captured into the result's separate stderr field, which the projector ignores.

[Console]::Error.WriteLine("Downloading...")  # → stderr (logged, ignored by projector)
Write-Output "#KANADE-INVENTORY-BEGIN"
Write-Output ($obj | ConvertTo-Json -Compress)
Write-Output "#KANADE-INVENTORY-END"

Fencing stdout payloads ensures clean extraction even if other output is present, and allows a single job script to provide inventory facts, health check state, and file collection metadata in a single run (#821). See configs/jobs/installers/scripts/install-kanade-client.ps1 for a worked example.

デフォルトで 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 を明示的にチェックしてください。

The agent does NOT auto-propagate a leftover $LASTEXITCODE — that would exit nonzero even when your script handled the native error gracefully. The launcher only re-raises an explicit exit N or a terminating error, so the job's exit code matches running your script with powershell -File. If you want the script's exit code to reflect a specific native call, propagate it yourself:

& git pull
if ($LASTEXITCODE -ne 0) { throw "git pull failed with exit code $LASTEXITCODE" }
# ネイティブの終了コードをそのまま伝播させたい場合は:
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 ケースには頼らないこと。
  • 協調的なキャンセルが必要なら、番兵ファイルやレジストリ値をポーリングして早めに抜けてください。エージェントからスクリプトへ、穏当に「切り上げろ」と伝える手段はありません。

実行中ジョブの kill

kanade kill <exec_id> は agent が購読する kill メッセージを publish します。受信すると agent は child.kill() を呼びます — timeout 経路と同じ hard-kill。operator にはただちに Killed マークの result row が届き、終了前に agent がキャプチャできた stdout / stderr が付きます。