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. その隣に launcher .ps1 を書き、UTF-8 コンソールエンコーディングを設定してから & '<your-script>' @args でユーザースクリプトを呼びます。
  3. 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_guiLocalSystem (ユーザーセッション内)✓ (ただし無意味、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 が付きます。