Compare commits

...
Author SHA1 Message Date
m 759328972a test: normalize mounted notification token
CI / test (pull_request) Successful in 10s
CI / test (push) Successful in 11s
2026-08-16 19:33:48 +03:00
m 01e1bdea1e fix: mount notification secret in commit runtime
CI / test (pull_request) Successful in 10s
CI / test (push) Successful in 11s
2026-08-16 19:29:17 +03:00
m 7a3280b007 deploy: add production notification delivery
CI / test (pull_request) Successful in 11s
CI / test (push) Successful in 10s
2026-08-16 19:25:19 +03:00
10 changed files with 246 additions and 3 deletions
+11
View File
@@ -0,0 +1,11 @@
assistent.cin.su {
encode zstd gzip
handle /delivery/* {
reverse_proxy host.docker.internal:18081
}
handle {
reverse_proxy host.docker.internal:18080
}
}
+29 -1
View File
@@ -30,6 +30,25 @@ services:
command: ["-js", "-m", "8222"]
volumes: ["assistant_nats:/data"]
notification-receiver:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: notification-receiver } }
restart: unless-stopped
environment:
LISTEN_ADDR: ":8085"
RECEIPT_DIRECTORY: /var/lib/notification-receiver
RECEIVER_WRITE_TOKEN_FILE: /run/notification-writer/token
RECEIVER_READ_TOKEN_FILE: /run/notification-reader/token
ports: ["${NOTIFICATION_RECEIVER_PORT:-18081}:8085"]
volumes:
- assistant_notification_receipts:/var/lib/notification-receiver
- assistant_notification_writer_secret:/run/notification-writer:ro
- assistant_notification_reader_secret:/run/notification-reader:ro
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8085/healthz >/dev/null"]
interval: 10s
timeout: 3s
retries: 10
core-runtime:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: core-runtime } }
restart: unless-stopped
@@ -82,8 +101,12 @@ services:
CORE_RUNTIME_URL: http://core-runtime:8080
CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081
COMMIT_EXECUTOR_TOKEN: ${COMMIT_EXECUTOR_TOKEN}
NOTIFICATION_WRITE_TOKEN_FILE: /run/notification-writer/token
NOTIFICATION_WRITE_SECRET_REF: domovoy-notification-write-v1
# External mutation credentials belong here only via Portainer/Docker secrets/env injection.
depends_on: [core-runtime, continuity-runtime]
depends_on: [core-runtime, continuity-runtime, notification-receiver]
volumes:
- assistant_notification_writer_secret:/run/notification-writer:ro
cognitive-worker:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: cognitive-worker } }
@@ -111,3 +134,8 @@ volumes:
assistant_nats:
assistant_core_secrets:
external: true
assistant_notification_receipts:
assistant_notification_writer_secret:
external: true
assistant_notification_reader_secret:
external: true
@@ -0,0 +1,83 @@
[CmdletBinding()]
param(
[string]$Endpoint = 'https://assistent.cin.su/delivery/v1/receipts?limit=100',
[string]$KeyRecordPath = "$env:LOCALAPPDATA\Domovoy\notification-receiver-read-key.dpapi.json",
[string]$StatePath = "$env:LOCALAPPDATA\Domovoy\notification-receiver-state.json",
[ValidateRange(1, 5)]
[int]$MaximumDisplays = 3,
[switch]$NoDisplay
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
function Write-AtomicJson {
param([Parameter(Mandatory)]$Value, [Parameter(Mandatory)][string]$Path)
$directory = Split-Path -Parent $Path
New-Item -ItemType Directory -Force -Path $directory | Out-Null
$temporary = Join-Path $directory ('.{0}.{1}.tmp' -f (Split-Path -Leaf $Path), [guid]::NewGuid().ToString('N'))
try {
[IO.File]::WriteAllText($temporary, (($Value | ConvertTo-Json -Depth 8) + "`n"), [Text.UTF8Encoding]::new($false))
Move-Item -LiteralPath $temporary -Destination $Path -Force
}
finally { if (Test-Path -LiteralPath $temporary) { Remove-Item -LiteralPath $temporary -Force } }
}
$uri = [Uri]$Endpoint
if ($uri.Scheme -ne 'https' -or $uri.Host -ne 'assistent.cin.su' -or $uri.AbsolutePath -ne '/delivery/v1/receipts') {
throw 'Notification receiver endpoint is outside the fixed HTTPS binding'
}
if (-not (Test-Path -LiteralPath $KeyRecordPath -PathType Leaf)) { throw "Notification receiver key not found: $KeyRecordPath" }
$keyRecord = Get-Content -LiteralPath $KeyRecordPath -Raw | ConvertFrom-Json
if ($keyRecord.version -ne 1 -or $keyRecord.project -ne 'domovoy' -or $keyRecord.purpose -ne 'notification receiver read') { throw 'Unsupported notification key record' }
$secure = ConvertTo-SecureString ([string]$keyRecord.protected_value)
$pointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
try { $token = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($pointer) }
finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($pointer) }
if ([string]::IsNullOrWhiteSpace($token)) { throw 'Notification receiver key is empty' }
try {
$response = Invoke-RestMethod -Uri $uri.AbsoluteUri -Headers @{ Authorization = "Bearer $token" } -TimeoutSec 15
}
finally { $token = $null; $secure.Dispose() }
$seen = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
if (Test-Path -LiteralPath $StatePath -PathType Leaf) {
$previous = Get-Content -LiteralPath $StatePath -Raw | ConvertFrom-Json
if ($previous.version -eq 1) { foreach ($id in @($previous.displayed_receipt_ids)) { [void]$seen.Add([string]$id) } }
}
$candidates = @($response.items | Where-Object {
$_.receipt_id -match '^receipt-[A-Za-z0-9_-]{1,128}$' -and
$_.action_intent_id -match '^[A-Za-z0-9_-]{1,128}$' -and
$_.request_digest -match '^[0-9a-f]{64}$' -and
$_.accepted_at_unix_millis -gt 0 -and
$_.payload.template_code -eq 'prospective-delivered-v1' -and
-not [string]::IsNullOrWhiteSpace([string]$_.payload.title) -and
-not [string]::IsNullOrWhiteSpace([string]$_.payload.message) -and
-not $seen.Contains([string]$_.receipt_id)
} | Sort-Object accepted_at_unix_millis | Select-Object -First $MaximumDisplays)
if (-not $NoDisplay -and $candidates.Count -gt 0) {
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$notification = [System.Windows.Forms.NotifyIcon]::new()
try {
$notification.Icon = [System.Drawing.SystemIcons]::Information
$notification.Visible = $true
$notification.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Info
foreach ($receipt in $candidates) {
$notification.BalloonTipTitle = ([string]$receipt.payload.title).Substring(0, [Math]::Min(63, ([string]$receipt.payload.title).Length))
$notification.BalloonTipText = ([string]$receipt.payload.message).Substring(0, [Math]::Min(255, ([string]$receipt.payload.message).Length))
$notification.ShowBalloonTip(6000)
Start-Sleep -Seconds 7
[void]$seen.Add([string]$receipt.receipt_id)
}
}
finally { $notification.Dispose() }
}
if (-not $NoDisplay) {
$bounded = @($seen | Select-Object -Last 1000)
Write-AtomicJson -Path $StatePath -Value ([ordered]@{ version = 1; displayed_receipt_ids = $bounded; last_poll_at_utc = [DateTime]::UtcNow.ToString('o') })
}
Write-Output "notification_receiver=poll_ok new=$($candidates.Count) displayed=$(if ($NoDisplay) { 0 } else { $candidates.Count })"
@@ -0,0 +1,40 @@
[CmdletBinding()]
param(
[string]$ScriptPath = '\\nas\MST\codex\assistent\deploy\windows\Invoke-DomovoyNotificationReceiver.ps1',
[string]$KeyRecordPath = "$env:LOCALAPPDATA\Domovoy\notification-receiver-read-key.dpapi.json",
[string]$NasKeyBackupPath = '\\nas\MST\codex\_backups\domovoy\keys\notification-receiver-read-key.dpapi.json',
[string]$TaskName = 'Domovoy Notification Receiver'
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
if (-not (Test-Path -LiteralPath $KeyRecordPath -PathType Leaf) -and (Test-Path -LiteralPath $NasKeyBackupPath -PathType Leaf)) {
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $KeyRecordPath) | Out-Null
Copy-Item -LiteralPath $NasKeyBackupPath -Destination $KeyRecordPath
}
if (-not (Test-Path -LiteralPath $KeyRecordPath -PathType Leaf)) { throw 'DPAPI-protected notification read key is missing' }
$record = Get-Content -LiteralPath $KeyRecordPath -Raw | ConvertFrom-Json
if ($record.version -ne 1 -or $record.project -ne 'domovoy' -or $record.purpose -ne 'notification receiver read') { throw 'Unsupported notification key record' }
$probe = ConvertTo-SecureString ([string]$record.protected_value)
$probe.Dispose()
$identity = [Security.Principal.WindowsIdentity]::GetCurrent().Name
& icacls.exe $KeyRecordPath /inheritance:r /grant:r "${identity}:F" | Out-Null
if ($LASTEXITCODE -ne 0) { throw 'DPAPI key ACL hardening failed' }
$nasDirectory = Split-Path -Parent $NasKeyBackupPath
New-Item -ItemType Directory -Force -Path $nasDirectory | Out-Null
$partial = "$NasKeyBackupPath.$PID.partial"
Copy-Item -LiteralPath $KeyRecordPath -Destination $partial
Move-Item -LiteralPath $partial -Destination $NasKeyBackupPath -Force
if (-not (Test-Path -LiteralPath $ScriptPath -PathType Leaf)) { throw 'Notification receiver script not found' }
$pwsh = (Get-Command pwsh.exe -ErrorAction Stop).Source
$arguments = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$ScriptPath`""
$action = New-ScheduledTaskAction -Execute $pwsh -Argument $arguments
$repeat = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval (New-TimeSpan -Minutes 2) -RepetitionDuration (New-TimeSpan -Days 3650)
$logon = New-ScheduledTaskTrigger -AtLogOn -User $identity
$principal = New-ScheduledTaskPrincipal -UserId $identity -LogonType Interactive -RunLevel Limited
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 2)
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger @($repeat, $logon) -Principal $principal -Settings $settings -Description 'Read typed Domovoy receipts and show local Windows notifications.' -Force | Out-Null
Write-Output "Scheduled task registered: $TaskName"
@@ -14,6 +14,7 @@ param(
'assistant-core-runtime-1',
'assistant-creator-web-1',
'assistant-commit-runtime-1',
'assistant-notification-receiver-1',
'assistant-continuity-runtime-1',
'assistant-cognitive-worker-1',
'assistant-postgres-1',
@@ -130,6 +131,14 @@ try {
}
catch { Add-Check 'offhost-backup-task' $false $_.Exception.Message }
try {
$notificationTask = Get-ScheduledTask -TaskName 'Domovoy Notification Receiver'
$notificationInfo = Get-ScheduledTaskInfo -TaskName $notificationTask.TaskName
$ok = $notificationTask.State -ne 'Disabled' -and $notificationInfo.LastTaskResult -eq 0
Add-Check 'notification-receiver-task' $ok "state=$($notificationTask.State) last_result=$($notificationInfo.LastTaskResult) next=$($notificationInfo.NextRunTime.ToString('o'))"
}
catch { Add-Check 'notification-receiver-task' $false $_.Exception.Message }
$overall = if (@($checks | Where-Object { -not $_.ok }).Count -eq 0) { 'healthy' } else { 'degraded' }
$record = [ordered]@{
version = 1
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000042-creator-notification-command.json",
"task_id": "DEV-000042",
"task_file": "dev/tasks/DEV-000043-production-notification-delivery.json",
"task_id": "DEV-000043",
"task_revision": 1
}
@@ -0,0 +1,37 @@
{
"task_id": "DEV-000043",
"task_revision": 1,
"architecture_revision": "ADR-0005",
"base_repo_revision": "c6139699a9efed624b49b15c801f5c04e884a0aa",
"risk_tier": "E",
"intent": "Deploy the accepted notification operation on docker-prod using available port 18081, fixed HTTPS routing, isolated writer/reader secret volumes, a durable receiver volume, and a current-user Windows display agent with DPAPI-protected read credential; verify an exact-artifact end-to-end receipt without docker-test.",
"requirement_ids": ["ADR-0005", "INV-AUTH-001", "INV-OWN-001", "INV-TOPO-001", "USER-DOCKER-PROD-ONLY"],
"allowed_change_scope": {
"modules": [],
"paths": [
"deploy/docker-compose.prod.yml",
"deploy/caddy/assistent.caddy",
"deploy/windows/Invoke-DomovoyNotificationReceiver.ps1",
"deploy/windows/Register-DomovoyNotificationTask.ps1",
"deploy/windows/Test-DomovoyProductionHealth.ps1",
"tests/creator_commit/verify-remote.ps1",
"docs/CREDENTIALS_AND_SECRETS.md",
"docs/DEPLOYMENT.md",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000043-production-notification-delivery.json"
]
},
"forbidden_change_scope": {"paths": ["architecture/**", "ci/**", ".gitea/**", "contracts/**", "modules/**", "adapters/**", "cmd/**", "database/**", "security/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 2, "new_third_party_dependencies": 0},
"contracts": {"consume": ["ADR-0005", "DEV-000041 receiver protocol", "DEV-000042 Creator command", "existing production compose and monitoring"], "may_change": []},
"semantic_owner": "no new canonical owner; receiver is external receipt state and Windows state is a rebuildable projection",
"canonical_state": ["CapabilityBinding inserted through Capability-owned schema", "Commit action/attempt evidence", "receiver immutable receipt volume"],
"state_transitions": ["install ACTIVE binding revision 1", "external receipt absent to present", "Windows local projection unseen to displayed"],
"side_effects_and_privileges": ["generate isolated write and read tokens outside Git", "deploy notification-receiver on docker-prod port 18081", "route only /delivery/* through assistent.cin.su", "mount write secret into Commit and receiver only", "mount read secret into receiver only", "register current-user Windows polling task"],
"persistence_and_migrations": ["apply migrations 010 and 011 on production", "create external secret volumes and durable receiver volume", "store Windows read token only as CurrentUser DPAPI ciphertext and back up ciphertext to the private NAS key directory"],
"failure_retry_restart": ["compose restart preserves receipts", "Windows polls read-only and deduplicates by receipt ID", "poll failure leaves receipts unseen for later retry", "receiver outage yields UNKNOWN_OUTCOME and read-only reconciliation", "no blind mutation retry"],
"assumptions": ["docker.cin.su SSH alias docker is the production host", "port 18081 is available", "Caddy host routing can reach host.docker.internal:18081", "assistent.cin.su TLS remains valid", "docker-test remains prohibited"],
"acceptance_scenarios": ["all expected containers including notification-receiver are healthy", "Caddy exposes receiver only under HTTPS /delivery/*", "unauthorized/write-with-read-token/arbitrary path requests are denied", "same intent and digest returns one receipt and mismatched digest conflicts", "production migration and binding match exact ADR fields", "Creator command yields SUCCEEDED or recoverable UNKNOWN_OUTCOME with durable evidence", "Windows agent reads without write privilege and displays/deduplicates the receipt", "restart preserves receipt and scheduled task succeeds", "no plaintext token appears in Git, task arguments, logs, database, browser storage or NAS"],
"required_tests": ["PowerShell parser", "unit", "architecture preflight", "exact diff scope", "go test", "go vet", "CI", "production compose health", "HTTPS receiver adversarial smoke", "Creator authenticated smoke", "receiver restart persistence", "Windows scheduled-task result", "browser console", "docker-test absence"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "BLOCKED_BY_MISSING_CAPABILITY"]
}
+2
View File
@@ -24,3 +24,5 @@ Each privileged runtime gets only its required secret/capability bindings. `comm
Creator authentication secrets are stored in the external Docker volume `assistant_core_secrets` and mounted read-only only at `/run/secrets` in `core-runtime`. The production container environment contains `_FILE` paths, not the secret values. Files are owned by runtime uid 100/gid 101 with mode 0400. `CREATOR_USERNAME` is not secret and remains an environment value. Development may continue to use direct environment values, but a direct value and its matching `_FILE` setting must never be configured together.
The PostgreSQL off-host backup passphrase is generated randomly and stored as CurrentUser DPAPI ciphertext at `%LOCALAPPDATA%\Domovoy\postgres-backup-key.dpapi.json`. An atomic copy of that DPAPI ciphertext, never the plaintext passphrase, is retained at `\\nas\MST\codex\_backups\domovoy\keys\postgres-backup-key.dpapi.json`. The passphrase is never stored in Git, the production Docker host, a scheduled-task argument, or NAS plaintext. The NAS backup directory receives only GnuPG AES-256 ciphertext and a non-secret integrity manifest. Recovery therefore requires both the NAS artifacts and the authorized Windows DPAPI profile.
Prospective notification delivery uses two unrelated random tokens. The write token is stored only in external Docker volume `assistant_notification_writer_secret`, mounted read-only into `commit-runtime` and `notification-receiver`. The read token is stored in external Docker volume `assistant_notification_reader_secret`, mounted read-only only into `notification-receiver`; its workstation copy is CurrentUser DPAPI ciphertext at `%LOCALAPPDATA%\Domovoy\notification-receiver-read-key.dpapi.json`. Only that ciphertext is backed up to `\\nas\MST\codex\_backups\domovoy\keys\notification-receiver-read-key.dpapi.json`. Plaintext tokens must not enter compose environment, Git, PostgreSQL, task arguments, logs, browser storage, or NAS.
+6
View File
@@ -14,6 +14,12 @@ Temporal is intentionally **not** the canonical owner of future obligations. If/
The initial stack definition is `deploy/docker-compose.prod.yml`. Portainer may deploy it from Git after the repository has been pushed.
The first external operation adds `assistant-notification-receiver-1` on production host port `18081`. Caddy applies `deploy/caddy/assistent.caddy`, routing only `https://assistent.cin.su/delivery/*` to that port and all other paths to Creator Web on `18080`. The receiver has an isolated durable volume and no Assistant database access. Commit success means an immutable receiver receipt exists; Windows display and human reading are not Commit outcomes.
Migrations `010_capability_notification_binding.sql` and `011_commit_notification_envelope.sql` must be applied before the new runtimes start. The initial Capability binding is owner-local for `domovoy-creator-principal`, revision 1, capability `prospective-notification-delivery`, origin `https://assistent.cin.su`, delivery path `/delivery/v1/deliveries`, receipt path `/delivery/v1/receipts`, protocol `IDEMPOTENT_RECEIPT_V1`, and secret reference `domovoy-notification-write-v1`.
`deploy/windows/Invoke-DomovoyNotificationReceiver.ps1` polls the receipt list with the read-only credential, displays up to three unseen local notifications, and records only rebuildable receipt IDs under `%LOCALAPPDATA%\Domovoy`. `Register-DomovoyNotificationTask.ps1` registers the current-user task every two minutes and at logon. It requires an interactive session for display and never uses `docker-test`.
Credentials/API deployment automation is deliberately not embedded. Use `PORTAINER_API_KEY` from an external secret source or deploy through Portainer UI/API after authenticating.
The single Creator login is configured with external `CREATOR_USERNAME` and `CREATOR_PASSWORD` values. They issue the same bounded Creator session and never select Actor, Principal, Scope or Authority. Keep `CREATOR_BOOTSTRAP_TOKEN` as a separate recovery/automation credential; none of these values belongs in Git, compose defaults for production, image layers or logs.
+27
View File
@@ -4,6 +4,7 @@ $ErrorActionPreference = "Stop"
$coreContainer = "assistant-core-runtime-1"
$commitContainer = "assistant-commit-runtime-1"
$continuityContainer = "assistant-continuity-runtime-1"
$receiverContainer = "assistant-notification-receiver-1"
function Post-Json([string]$container, [string]$url, [object]$body) {
$json = $body | ConvertTo-Json -Depth 10 -Compress
@@ -101,4 +102,30 @@ if ($updated.State -ne "SATISFIED" -or $updated.Revision -ne 2) { throw "Creator
Expect-CreatorStatus http://localhost:8082/v1/creator/continuity/satisfy-delivered $command $creatorSession 409
Expect-CreatorStatus "http://localhost:8082/v1/creator/authorized-actions/$($action.AuthorizedActionID)/reconcile" @{} $creatorSession 409
$notificationProspective = "prospective-notification-$suffix"
Post-Json $continuityContainer http://localhost:8081/v1/prospective-items @{
ID=$notificationProspective;CommitmentID="commitment-notification-$suffix";OwnerPrincipalID="domovoy-creator-principal"
ScopeID=$scope;GoalID=$goal;TriggerAtUnixMillis=1800000000000;State="DELIVERED";Revision=1
} | Out-Null
$notificationCommand = @{
Target=@{TaskID=$task;GoalID=$goal;ScopeID=$scope;Revision=1}
ProspectiveItemID=$notificationProspective;ExpectedRevision=1;Confirm=$true
}
Expect-CreatorStatus http://localhost:8082/v1/creator/continuity/notify-delivered $notificationCommand "invalid" 401
$hostileNotification = $notificationCommand.Clone()
$hostileNotification.URL = 'https://example.invalid'
$hostileNotification.Body = 'arbitrary'
Expect-CreatorStatus http://localhost:8082/v1/creator/continuity/notify-delivered $hostileNotification $creatorSession 400
$notificationAction = Creator-Post http://localhost:8082/v1/creator/continuity/notify-delivered $notificationCommand $creatorSession | ConvertFrom-Json
if ($notificationAction.State -ne 'SUCCEEDED' -or -not $notificationAction.ActionIntentID) { throw 'Creator notification did not produce a successful receipt' }
$notificationItem = docker --context $DockerContext exec $continuityContainer wget -qO- "http://localhost:8081/v1/prospective-items/$notificationProspective" | ConvertFrom-Json
if ($notificationItem.State -ne 'DELIVERED' -or $notificationItem.Revision -ne 1) { throw 'Notification changed Continuity canonical state' }
$receiptRaw = docker --context $DockerContext exec $receiverContainer sh -c 'token=$(tr -d "\r\n" < /run/notification-reader/token); wget -qO- --header="Authorization: Bearer $token" "$1"' sh "http://localhost:8085/delivery/v1/receipts/$($notificationAction.ActionIntentID)"
if ($LASTEXITCODE -ne 0) { throw 'Notification receipt lookup failed' }
$receipt = ($receiptRaw -join "`n") | ConvertFrom-Json
if ($receipt.action_intent_id -ne $notificationAction.ActionIntentID -or $receipt.request_digest -notmatch '^[0-9a-f]{64}$') { throw 'Notification receipt evidence mismatch' }
$notificationHistory = Creator-Get 'http://localhost:8082/v1/creator/authorized-actions?limit=50' $creatorSession | ConvertFrom-Json
$notificationHistoryItem = @($notificationHistory.Items | Where-Object { $_.AuthorizedActionID -eq $notificationAction.AuthorizedActionID })
if ($notificationHistoryItem.Count -ne 1 -or $notificationHistoryItem[0].Operation -ne 'deliver-prospective-notification-v1' -or $notificationHistoryItem[0].State -ne 'SUCCEEDED') { throw 'Notification action history is incomplete' }
Write-Output "Creator Commit command verification: PASS"