Phase 1 (plugin side) of docs/ADMIN_CONTROLS.md: a staff write plane so the website can moderate the live shard. - BridgeAdmin.cs: inbound admin.kick, admin.ban (timed + indefinite), admin.unban, admin.broadcast. Each requires an `actor`, refuses targets at or above AdminAccessFloor (default CoOwner — Owner-only shield), replies admin.ok/admin.error with the reqId echoed, and emits an admin.audit (origin=web) broadcast. Attribution is web:<actor> in the console log and the ban BanDealer tag. Kicking enumerates NetState.Instances so a character-select session is caught too. - BridgeConfig/Bridge.cfg: AdminWriteEnabled (default OFF — opt-in), AdminAccessFloor, broadcast/reason length caps, ban duration clamp. - tools/stub_sidecar_admin.ps1: live smoke-test harness; *.log gitignored. Verified: compiles clean against ServUO (0 err/warn); live run on the seeded shard confirms all four verbs, the audit stream, timed-ban fields, and the Owner-floor refusal, with no exceptions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0114TpmrNW4wNXsHq5CR72jQ
79 lines
2.8 KiB
PowerShell
79 lines
2.8 KiB
PowerShell
param(
|
|
[int] $Port = 7788,
|
|
[string] $Log = "$PSScriptRoot\sc_admin.log"
|
|
)
|
|
|
|
# Phase-1 admin write-plane harness. Connects as the sidecar, waits for the shard,
|
|
# fires admin.* commands covering the happy paths and every guard, logs the replies.
|
|
# Requires Bridge.cfg AdminWriteEnabled=true and the seeded world (seed_00x accounts).
|
|
|
|
function Say($msg) {
|
|
for ($i = 0; $i -lt 5; $i++) {
|
|
try { "$msg" | Out-File -FilePath $Log -Append -Encoding utf8; return }
|
|
catch { Start-Sleep -Milliseconds 100 }
|
|
}
|
|
}
|
|
|
|
"" | Out-File -FilePath $Log -Encoding utf8
|
|
Say "[admin] starting on 127.0.0.1:$Port"
|
|
|
|
$listener = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Loopback, $Port)
|
|
$listener.Server.SetSocketOption('Socket', 'ReuseAddress', $true)
|
|
|
|
$bound = $false
|
|
for ($i = 0; $i -lt 30 -and -not $bound; $i++) {
|
|
try { $listener.Start(); $bound = $true }
|
|
catch { Start-Sleep -Seconds 1 }
|
|
}
|
|
if (-not $bound) { Say "[admin] could not bind"; exit 1 }
|
|
|
|
Say "[admin] listening"
|
|
$client = $listener.AcceptTcpClient()
|
|
Say "[admin] === shard connected ==="
|
|
|
|
$stream = $client.GetStream()
|
|
$reader = New-Object System.IO.StreamReader($stream)
|
|
$writer = New-Object System.IO.StreamWriter($stream)
|
|
$writer.AutoFlush = $true
|
|
|
|
Start-Sleep -Milliseconds 500
|
|
|
|
$requests = @(
|
|
# happy path, no target needed
|
|
'{"kind":"admin.broadcast","reqId":"a-bcast","actor":"whitlocktech","text":"uo-link admin test broadcast"}',
|
|
# ban an offline seed account (timed), then unban
|
|
'{"kind":"admin.ban","reqId":"a-ban","actor":"whitlocktech","account":"seed_001","durationSec":3600,"reason":"harness test"}',
|
|
'{"kind":"admin.unban","reqId":"a-unban","actor":"whitlocktech","account":"seed_001"}',
|
|
# kick an offline account -> should succeed with sessions:0
|
|
'{"kind":"admin.kick","reqId":"a-kick","actor":"whitlocktech","account":"seed_002"}',
|
|
# floor: whitlocktech is Owner -> must be refused
|
|
'{"kind":"admin.ban","reqId":"a-floor","actor":"whitlocktech","account":"whitlocktech"}',
|
|
# unknown target
|
|
'{"kind":"admin.ban","reqId":"a-unknown","actor":"whitlocktech","account":"does_not_exist"}',
|
|
# missing actor -> refused by the shared gate
|
|
'{"kind":"admin.ban","reqId":"a-noactor","account":"seed_003"}'
|
|
)
|
|
|
|
foreach ($r in $requests) {
|
|
$writer.WriteLine($r)
|
|
Say "[admin] -> $r"
|
|
Start-Sleep -Milliseconds 400
|
|
}
|
|
|
|
# Drain greedily: block on ReadLine with an idle timeout so a buffered burst is fully read
|
|
# (the DataAvailable-gated pattern drops the tail of a burst that a StreamReader pre-buffers).
|
|
$stream.ReadTimeout = 2500
|
|
try {
|
|
while ($true) {
|
|
$line = $reader.ReadLine()
|
|
if ($null -eq $line) { break }
|
|
Say "[admin] <- $line"
|
|
}
|
|
} catch {
|
|
Say "[admin] read window closed (idle)"
|
|
}
|
|
|
|
Say "[admin] done"
|
|
$client.Close()
|
|
$listener.Stop()
|