Empirical evidence: MDM baseline policy push lands well within 60s
after PPKG triggers immediate reboot path on bays where assignment
filter matches. Bays where it doesn't deliver in 60s aren't going
to deliver in 180s either - they're blocked on an assignment-filter
or dynamic-group lag (sometimes 30+ min in GCC-High), not on the
raw sync window. Trimming 120s of dead wait off every imaging cycle.
Aggressive 30s Schedule #3 hammer + early-exit on baseline (>=5
subkeys) preserved - those still help bays that DO deliver fast.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Local-user shopfloor fleet (ShopFloor is a LOCAL account) means
AzureAdPrt stays NO, user-scoped Intune policies never deliver, and
the natural completion gate (baseline policies >= 5 + DSCInstall
success + Phase 4 wrappers) never closes. Dashboard sessions stuck
at 7/8 / 87.5% forever even though the bay is functionally complete.
Real-world definition of "done" for these bays is lockdown applied.
Add a per-tick check in Get-Phase1 (alongside the DeviceId push) that
detects either:
* Winlogon DefaultUserName -like 'ShopFloor*' AND AutoAdminLogon=1
* OR C:\Enrollment\force-lockdown-applied.txt marker file
When either is present and not yet pushed, fire Send-PxeStatus
StageIndex=8 / StageTotal=8 / Status='succeeded' with the captured
IntuneDeviceId for the dashboard QR. One-shot per session via the
LockdownCompletePushed cache flag.
No need for the operator to run mark-complete.bat anymore -
Monitor's main loop will fire idx=8 within ~5s of force-lockdown
or autologon flip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Monitor-IntuneProgress.ps1: the previous Ensure-SendPxeStatus function
ran '. $lib' from inside the function body. PowerShell's dot-source-
inside-function semantics put the imported Send-PxeStatus into the
function's LOCAL scope, not the script scope. By the time Get-Phase1
called Get-Command Send-PxeStatus, the function had already returned
and Send-PxeStatus was out of scope - silently never invoked, no log
entry at all (success or failure). Diagnostic confirmed: bay had
DeviceId in dsregcmd, manual Send-PxeStatus from operator prompt
fired idx=7 cleanly with QR rendered, but Monitor's automatic call
never showed up in C:\Logs\send-pxe-status.log.
Fix: dot-source at script top-level (outside any function). Then
Send-PxeStatus is in script scope where every function in the file
can call it. Keep Ensure-SendPxeStatus as a no-op stub for any caller
still invoking it.
imaging.html: bump QR data-qr-size from 56 to 160 px. A 36-char UUID
at ECC M needs ~29x29 modules; at 56px each module was ~1.5px which
is too tight for a phone camera to lock onto from typical distance.
160 px gives ~5 px/module which scans cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PowerShell parses $var: as scope-namespaced syntax (e.g. $env:NAME,
$global:foo). The line
Log "server=$PxeServer:$Port pctype=$PCType"
errored at line 26 col 13 - parser interpreted $PxeServer: as a scope
prefix and bailed. Fix: use ${PxeServer}:${Port} so the colon is
literal. The $uri line below already had the right form.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DeviceId capture was nested inside the -not AzureAdJoined gate. Once
AAD joined flipped true the block stopped running, but DeviceId only
appears in dsregcmd output AFTER AzureAdJoined is set, so the capture
never fired and Send-PxeStatus -IntuneDeviceId never pushed. Webapp
session JSON missing intune_device_id field; /imaging card couldn't
render the QR even though the bay-side Build-QRCodeText showed the
QR correctly (it calls dsregcmd each render with no gate).
Fix: change the gate condition so the dsregcmd call keeps running
while EITHER AzureAdJoined OR DeviceId is still missing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Run-ShopfloorSetup.ps1 is copied by startnet.cmd to W:\Enrollment\
(root of Enrollment, NOT inside shopfloor-setup/). So $PSScriptRoot is
W:\Enrollment\. The dot-source path was Join-Path $PSScriptRoot
'Shopfloor\lib\Send-PxeStatus.ps1' which resolves to
W:\Enrollment\Shopfloor\lib\Send-PxeStatus.ps1 - that path does not
exist.
The actual file lands at W:\Enrollment\shopfloor-setup\Shopfloor\lib\
Send-PxeStatus.ps1 (xcopied by startnet from the Shopfloor share dir
into the shopfloor-setup\ subdir). Test-Path returned false, dot-source
silently skipped, Send-PxeStatus was never defined, every Report-Stage
call no-op'd, no log file was written, no POSTs reached the dashboard.
Symptom: bay reaches Windows desktop + runs Run-ShopfloorSetup but
never appears on /imaging dashboard. C:\Logs\send-pxe-status.log does
not exist on the bay.
Fix: add the missing 'shopfloor-setup\' segment so the path resolves
to the actual file location.
09-Setup-*.ps1 use a different relative path ('..\Shopfloor\lib\...')
from inside the per-type dir and were unaffected. Monitor-IntuneProgress
sits in Shopfloor\lib already and uses a sibling lookup - also fine.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The inline one-liner in startnet.cmd called Get-NetAdapter, which is
not available in WinPE's stripped PowerShell (no NetTCPIP module).
Errors silently swallowed by the surrounding try/catch - POST never
fired, dashboard never showed bays during the WIM-apply phase.
Externalize to a standalone .ps1 on the enrollment share:
* Uses wmic (always present in WinPE 10+) for both serial AND mac
instead of Get-CimInstance / Get-NetAdapter.
* Logs every step to X:\Windows\Temp\winpe-status-push.log so a
future "POST didn't fire" debug is one file read away.
* startnet.cmd now just runs powershell -File Y:\scripts\winpe-status-
push.ps1. Future edits to the push logic do NOT require a boot.wim
rebuild; just edit the .ps1 on the share.
Mirror the existing pattern for run-enrollment.ps1 / wait-for-internet.ps1
/ migrate-to-wifi.ps1 (all already at /srv/samba/enrollment/scripts/).
Add the new file to the playbook's enrollment-scripts copy loop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related changes so the /imaging dashboard reaches 100% and so the
operator can see why POSTs are not arriving when a session stalls.
Monitor-IntuneProgress.ps1:
* After sync-complete.txt is written (DSC + lockdown done) fire a
final Send-PxeStatus -StageIndex 8 -StageTotal 8 -Status 'succeeded'
+ IntuneDeviceId. Previously the script exited without any final
status push, so even a perfect run capped at idx=7 / 87.5%. The
session now reaches 8/8 / 100% green when imaging actually finishes.
Send-PxeStatus.ps1:
* Log EVERY POST attempt (both success and failure) to C:\Logs\
send-pxe-status.log with idx, status, stage name, and either the
HTTP code on success or the exception message on failure. Was
previously silent-on-success, log-on-failure. Operator can now
correlate dashboard state to actual outbound activity:
OK idx=2/8 status=in_progress http=200 stage='Run-ShopfloorSetup: starting'
ERR idx=2/8 status=in_progress uri=http://10.9.100.1:9009/... err=Unable to connect
* Errors still swallowed - imaging never blocks on a failed status push.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The existing _ntlars-backups/*.reg files are used by the automated
imaging pipeline. They are REGEDIT5 UTF-16 with explicit WOW6432Node
in the key path. That works for automated reg-import during install
but the NTLARS Load... button in the NTLARS settings dialog rejects
them - NTLARS expects:
* REGEDIT4 ANSI / CRLF / no BOM
* Bare path: HKLM\SOFTWARE\GE Aircraft Engines\DNC\...
(no \WOW6432Node\ - NTLARS is 32-bit, Windows redirector handles
the mapping transparently when NTLARS reads/writes)
* No semicolon comment header
This commit generates the parallel 147-file tree at
playbook/shopfloor-setup/_ntlars-backups-manual/ derived from the
existing _ntlars-backups/ files. Content (FMSHostPrimary +
FMSHostSecondary edits from df443d5 + 802d85e) is preserved; only
format and path are transformed. Both trees coexist - automation
continues to pull from _ntlars-backups/, operators use _ntlars-
backups-manual/ when they need to Load... a bay's reg manually.
Also live at /srv/samba/enrollment/shopfloor-setup/_ntlars-backups-
manual/ on the PXE server (\\10.9.100.1\enrollment) for share-based
access by the operator.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reverts the 2026-04-24 no-op stub. Empirically the gateway-suppression
fix (dnsmasq dhcp-option=3/=6 empty) alone is NOT sufficient to keep
Windows from using the wired NIC for Intune Device Configuration / DSC
traffic. Even with no default route on wired AND the unattend's
InterfaceMetric trick (WiFi=10, Wired=100), the bay stalls on the DSC
phase until the wired cable is physically unplugged.
Restoring the prior body that disables non-WiFi NICs at first logon
post-PPKG. Gated on Get-NetAdapter for a Wi-Fi/Wireless/WLAN/802.11
adapter - towers without WiFi stay on ethernet (the only-NIC scenario
where disabling would hang first logon). Falls back to re-enabling
ethernet if login.microsoftonline.us:443 doesn't respond within 5 min.
Monitor-IntuneProgress.ps1 already has the symmetric re-enable
("Re-enable any wired NICs that Order 5 disabled") at the start of its
monitor loop, which kicks in after DSC creds land. Net effect: wired
disabled during the DSC fetch window, re-enabled by the time eDNC
autostart needs the local NIC.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VM-test-confirmed root cause for the imaging "Setup wizard prompt"
hang. The bundled MSI has CustomAction caDriverInstall_x64 that
invokes dpinst.exe at install time:
caDriverInstall_x64 type=3666 (exe, deferred) Source=dpinst.exe
Condition: (Not Installed) And VersionNT64 And (VersionNT>=601)
Sequence: 6505
The bundled dpinst.xml is minimal (<dpInst><legacyMode/></dpInst>) -
no <quietInstall/> directive - so dpinst pops its wizard and waits
for operator click-through. /qn on the parent MSI does NOT propagate
into the dpinst child process. pnputil pre-staging the INF + cert
pre-trust to TrustedPublisher does NOT prevent the CA from firing
(the CA runs unconditionally on first install regardless of
DriverStore presence).
Fix: msibuild patch the MSI's InstallExecuteSequence to set the
action's Condition column from
"(Not Installed) And VersionNT64 And (VersionNT>=601)"
to
"0"
which evaluates false on every install attempt - the action never
fires, dpinst never runs, no wizard pops.
The driver itself is now installed exclusively by:
1. our pnputil pre-stage in 09-Setup-Keyence.ps1 (already there),
2. the manifest's separate "KEYENCE VR Series USB Driver" INF entry.
End-to-end VM test: 36s, exit 0, VR-6000 DisplayVersion 4.3.7
detected, zero dpinst processes at finish.
MSI size 1751552 -> 1744896 bytes (msibuild table rewrite).
md5: c6edcfc6c6808617598bcb7a15072a30.
Backups of original MSI on live PXE server enrollment share + local
SFLD share mirror as .bak-<TS>-orig.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the stage indices reflected logical milestones but not the
order they fire in. Run-ShopfloorSetup posted idx=1 (start) and idx=4
(PPKG) - but 09-Setup-Keyence (inside per-type loop) ran BETWEEN them
and posted idx=5/6. The dashboard then "regressed" from 6 back to 4
when PPKG fired, making it look stuck at the per-type-complete card.
New numbering matches actual execution order:
1 - WinPE: PESetup / WIM apply (startnet.cmd)
2 - Run-ShopfloorSetup: starting (Run-ShopfloorSetup.ps1)
3 - 09-Setup-<Type>: starting (per-type)
4 - 09-Setup-<Type>: complete (per-type)
5 - Run-ShopfloorSetup: PPKG enrollment (Run-ShopfloorSetup.ps1)
6 - Run-ShopfloorSetup: handoff to Monitor (Run-ShopfloorSetup.ps1)
7 - Monitor-IntuneProgress: Intune Device ID captured
services/imaging_status.py rewind threshold reverts to stage_index <= 1
now that WinPE startnet posts idx=1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reset trigger previously fired only when a new POST landed at idx <= 1,
which meant a reimage didn't reset the dashboard card until
Run-ShopfloorSetup ran post-PPKG (~10-20 min in). With the WinPE-phase
status push from startnet.cmd in commit 4e018fe firing at idx=2, that
earlier signal needs to count as a new-run marker too.
Threshold of 2 makes startnet.cmd the canonical reset point: within
seconds of PXE menu choice on the bay, the dashboard card flips from
the previous run's high-idx state back to "WinPE: PESetup / WIM apply"
+ fresh started_at.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the filename, type, and size columns from the Blancco Reports
list - operators want bay-identification fields, not file metadata.
Filename moves to a row hover tooltip (title attribute) so it is still
recoverable for ad-hoc lookups.
Adds a Result column derived from each XML report's overall erasure
state:
* Successful -> green badge (all erasure entries report Successful)
* Failed -> red badge (any erasure entry reports a non-Successful state)
* other -> grey badge with the verbatim state
* blank/non-XML -> dash
The state roll-up lives in the blancco_reports route's per-file parse
loop next to the existing serial/model extraction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
services/imaging_status.py - if a new POST arrives with stage_index <= 1
that is lower than the cached stage_index, OR the previous run already
finished (status=succeeded|failed), reset the session: clear log_tail,
mint a fresh started_at, drop the status field so the in_progress
default re-applies. Preserves serial + records the previous run's
last_updated under previous_run_at for audit. Without this, a reimage
on the same bay would leave a stale 6/8 "succeeded" card visible until
the new run progressed past that index.
playbook/startnet.cmd - one-line PowerShell POST after the PXE menu
choice + enrollment-share mount, before PESetup.exe waits to start.
Captures BIOS serial via wmic, MAC via Get-NetAdapter, and posts:
stage_index=2, current_stage="WinPE: PESetup / WIM apply".
Best-effort; try/catch swallows any network failure so a missing
webapp never blocks imaging. PXE clients will now appear on the
/imaging dashboard during WinPE phase instead of only post-PPKG.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the imaging-progress helper into the three PC-type setup scripts
that were either clean (CMM) or untracked (Common, Heattreat). Each
gains two calls per the pattern committed for Keyence in 9122b28:
* idx 5/8 - "09-Setup-<Type>: starting" right after the session start banner
* idx 6/8 - "09-Setup-<Type>: complete" just before the completion banner
Display, Genspect, and WaxAndTrace also got the same two-line additions
locally and on the live server, but those files have pre-existing WIP
edits intermixed so they aren't staged here. They'll travel along
when the operator commits their unrelated shopfloor work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds end-to-end progress tracking for PXE imaging sessions and surfaces
each Blancco report's BIOS serial in the report list.
webapp:
* services/imaging_status.py - JSON-per-serial state store under
IMAGING_DIR (default /var/log/pxe-imaging). Atomic write via
tempfile + rename. log_tail capped at 50 lines. Merges partial
updates so clients can post just the current_stage tick.
* config.py - new IMAGING_DIR env-overridable path.
* services/csrf.py - explicit exempt list for machine-to-machine
endpoints; /imaging/status is the first entry. Air-gapped LAN;
trust-by-network for client posts.
* app.py - four new routes:
GET /imaging dashboard (renders all sessions)
POST /imaging/status client status push (JSON body)
GET /imaging/<serial>.json raw session JSON for ad-hoc polling
POST /imaging/delete/<s> clear a session from the dashboard
Also parses each Blancco XML in the /reports list to surface
system.serial + system.model columns.
* templates/imaging.html - Bootstrap dashboard with per-session
cards (state badge, progress bar, stage idx/total, mac, elapsed,
log tail). meta http-equiv refresh=5 for auto-tick.
* templates/base.html - new "Imaging Progress" nav entry.
* templates/reports.html - Serial + Model columns added.
playbook:
* shopfloor-setup/Shopfloor/lib/Send-PxeStatus.ps1 - new helper.
Dot-source this then call Send-PxeStatus -Stage X -StageIndex N
-StageTotal M from any stage script. BIOS serial via CIM, MAC via
Get-NetAdapter, pctype + machinenumber from C:\Enrollment.
Failures are swallowed to a local log so a network blip doesn't
block imaging.
* shopfloor-setup/Run-ShopfloorSetup.ps1 - dot-sources helper +
posts at three coarse milestones (start, PPKG enrollment,
handoff to Monitor-IntuneProgress).
* shopfloor-setup/gea-shopfloor-keyence/09-Setup-Keyence.ps1 -
posts at session start + after Install-FromManifest with
succeeded/failed status derived from $rc. Other 09-Setup-*.ps1
scripts can follow the same pattern.
ID is BIOS serial (stable across WinPE -> Windows transition and
across reboots, unlike hostname which is random pre-PPKG). Operator
already knows the serial of the bay they imaged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VR-6000 Series Software.msi is an InstallShield MSI that references
Data1.cab in the same directory for its compressed payload. The cab was
never staged into the repo's keyence installers/ dir, so msiexec exited
1603 with "SECREPAIR: Failed to open the file ... Data1.cab" on every
imaging run (see Logs/Keyence/install.log on a failed bay for the
canonical signature). Only the 1.75 MB MSI was committed; the 560 MB
cab lives on the GE-Enforce SFLD share at
tsgwp00525\sfld$\v2\shared\dt\shopfloor\gea-shopfloor-keyence\apps\.
This commit doesn't add the cab itself (560 MB; same gitignore convention
as PrinterInstallerMap.exe and other large binaries). Instead it pins the
staging requirement in two places:
* .gitignore: explicit entry with the SFLD share path so a future
operator wiring up a fresh PXE server build knows where to source it.
* keyence-manifest.json _comment: documents the dependency next to the
MSI declaration that needs it.
The local repo at /home/camp/projects/pxe now has the cab staged in
playbook/shopfloor-setup/gea-shopfloor-keyence/installers/ for the next
USB build. Rebuilding the Keyence image and re-imaging the failed bay
should now reach DisplayVersion 4.3.7 detection successfully.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end fixes for Blancco Drive Eraser PXE flow uncovered by chasing
"reports never reach SMB share" across two air-gapped sites:
playbook/blancco-init.sh:
* Drop silent || true on wget of preferences.xml + config.xml. Fail
loud with shell-drop if download or marker grep fails. Background:
airootfs /opt/scripts/validate_preferences.sh restores
/albus/preferences.save (factory defaults, empty network_share) if
xmllint fails. wget failure made every report silently land nowhere.
* Clobber /albus/preferences.save with the same served file so even if
the validator fallback fires, the SMB target survives.
* Bind-mount /dev/null over /sys/power/{state,disk,mem_sleep,autosleep}
before switch_root. Albus's license-retry path writes /sys/power/state
directly (bypassing systemd targets); this is the last-line block.
* /dev/null symlinks for sleep/suspend/hibernate systemd targets in the
airootfs overlay + logind drop-in with IdleAction/Handle*=ignore.
Three independent layers because cmdline systemd.mask alone is bypassed
by direct /sys/power/state writes.
* xinitrc.d/00-no-screen-blank.sh runs xset s off -dpms + setterm
-blank 0 -powerdown 0 so the Blancco GUI doesn't blank during long
erasures.
* Removed the 20-failsafeDriver.conf "modesetting" pin. modesetting
needs DRM/KMS which we disable on kernel cmdline; "vesa" also failed
on NVIDIA. With the pin gone Xorg auto-picks fbdev which uses the
kernel framebuffer from vga=normal - works across Intel, AMD, and
older NVIDIA without nouveau.
playbook/pxe_server_setup.yml:
* dnsmasq.conf: explicit empty-value dhcp-option=3 + dhcp-option=6.
Without them, dnsmasq defaults to sending its own IP as router AND
DNS. Commenting the configured-value lines did NOT disable the push
(root cause of "wired keeps picking up 10.9.100.1 as gateway").
* Split the Blancco config.img extraction and preferences.xml deploy
into separate tasks. The previous shell-with-creates: gate caused
playbook re-runs to skip the prefs deploy entirely after first run.
* Added a validation task that runs python3 xml parse + grep on the
deployed preferences.xml to fail the playbook at deploy time if the
SMB markers are missing.
* Added Environment=TZ=America/New_York to the pxe-webapp systemd
service so report mtimes and audit log render in Eastern time even
if the Python process is started before timedatectl converges.
webapp:
* services/blancco_report.py: parse Blancco's XML report format
(recursive <entries name="..."> walker) into a friendly dict.
* templates/report_view.html: Bootstrap "Drive Erasure Certificate"
layout - hero summary, customer + system cards, per-drive cards with
step-by-step erasure timeline, document signing footer with
integrity hash detail.
* /reports/view/<filename> route + View button on the reports list
(XML reports only; PDFs still download).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Legacy-BIOS PXE clients booting Blancco reported "NBP is too big to
fit in free base memory". Cause: dnsmasq unconditionally served
ipxe.efi (~675KB EFI binary) which legacy BIOS PXE ROMs cannot
execute and which exceeds their NBP cap.
Fix:
- Add undionly.kpxe (~70KB BIOS-mode iPXE, from boot.ipxe.org).
- dnsmasq: dhcp-match on option:client-arch,0 (BIOS) -> undionly.kpxe;
default (everything else, including UEFI x86_64 arch 7 and 9) keeps
getting ipxe.efi. Tag form is reversible: if the match fails to
evaluate, fallback is the working EFI path, not the new binary.
- Ansible TFTP-copy loop: mirror undionly.kpxe alongside ipxe.efi.
- .gitignore exception: track the open-source kpxe binary so the
air-gapped USB build stays self-contained.
UEFI clients unchanged. Blancco/Clonezilla/WinPE chain after the
iPXE menu is identical regardless of which iPXE variant delivered it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tech catches a PC imaged with a wrong machine number. Previously the
share restore (NTLARS .reg + UDC settings + UDC live data) only fired
on the placeholder->real transition, so a real->real change rewrote
only UDC JSON, eDNC reg, and MTConnect Devices.xml - leaving the wrong
NTLARS config in place.
Update-MachineNumber.ps1: replace the placeholder-only guard with an
any-change guard so the share restore block fires on reassign too.
The existing one-shot migrated/ consumption keeps live-data restore
idempotent. Also writes C:\Enrollment\machine-number.txt to keep
imaging-time scripts in sync.
Set-MachineNumber.ps1 (both collections + nocollections): show a
confirmation dialog when reassigning between two real numbers, naming
old/new and listing what gets pulled. Audit each call to
C:\Logs\Shopfloor\reassign.log.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step-by-step runbook for techs at the imaged PC. Two symptoms covered:
UDC not collecting data (admin-unlock + COM port walkthrough) and DNC
not pushing to controller (NTLARS reg restore + FMS Host FQDN + Realtek
PCIe GbE static IP). Mermaid overview links to each section. Live HTML
uses CDN; static HTML pre-renders SVG for offline / printable use.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The GE Aerospace Shop Floor MCE image type was unused (never had
content on the live PXE server) and cluttered the dashboard. The
GE Legacy Shop Floor MCE entry stays.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The playbook deploys SFLD provisioning packages to
/srv/samba/enrollment/ppkgs/ but the /enrollment route scanned the
share root. Result: every visit reported "no enrollment packages
found" even though three .ppkg files were present.
Add ENROLLMENT_PPKG_DIR (defaults to {ENROLLMENT_SHARE}/ppkgs,
overridable via env var) and point all four enrollment routes at it
(list, upload, download, delete).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "Configure static IP for PXE interface" task writes a flat
single-NIC netplan config. Live PXE server (10.9.100.1) overrides
this with a bridge config bonding the USB-C 5 Gbps NIC and the
onboard NIC into br-pxe, because the onboard NIC alone cannot
sustain the imaging throughput required by the floor.
Add a comment block above the task warning that re-running it on
a box already using the bridge config will replace the bridge with
a flat single-iface config and likely break the PXE LAN. The full
bridge YAML is included for reference. Recovery is via
/etc/netplan/50-cloud-init.yaml.pre-gold-swap (preserved by netplan
backup).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Models.txt entry maps "7080" substring (matches WMI csproduct name
"OptiPlex 7080") to OptiPlex_7080_1.37.0.exe. BIOS .exe already
deployed to /srv/samba/winpeapps/_shared/BIOS/ on the live PXE
server via download-drivers.py.
Also adds docs/geastandardpbr-overrides.md tracking the local
geastandardpbr/ edits (user_selections.json + HardwareDriver.json
get a 7080 entry under "D11 OptiPlex Family") that the gitignore
prevents from being tracked directly. Includes a Python snippet
to idempotently re-apply after a fresh USB import.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three related fixes:
1. Hard-coded BIOS push path was /srv/samba/enrollment/BIOS, which does
not exist on the live PXE server. Real path is the shared
/srv/samba/winpeapps/_shared/BIOS/ where check-bios.cmd lives and
playbook task pxe_server_setup.yml:485 deploys Flash64W.exe + the
per-model BIOS .exe files.
2. Generated models.txt was 2-column (ModelSubstring|BIOSFile) but
check-bios.cmd reads tokens=1,2,3 with delims=| and uses field 3
for the version compare. Without the 3rd column, the version-skip
logic never engages and every imaged PC re-flashes BIOS on every
boot. Now writes 3-column (ModelSubstring|BIOSFile|Version).
3. The script overwrote the live models.txt with only the entries it
touched in the current run. Live had 50+ entries; a single-model
run wiped the other 49. Now prints the lines and asks the operator
to merge them into playbook/shopfloor-setup/BIOS/models.txt and
re-deploy via scripts/deploy-bios.sh.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Display kiosk user cannot authenticate to the tsgwp00525 SFLD share,
so any share-dependent enforcement task on Displays would fail every
cycle. Display is now self-contained: kiosk EXE installs at imaging
time via preinstall.json (Install-KioskApp.cmd) and Edge kiosk
policies via 09-Setup-Display.ps1. No ongoing SFLD-share dependency.
Gate both registrations behind a $noEnforceTypes alias group so
either pcType form (Display, gea-shopfloor-display) hits the skip
path. Other PC types still register both tasks unchanged.
Verified on win11 VM: matrix test confirmed Display + gea-shopfloor-
display SKIP both gates while Standard / CMM / gea-shopfloor-
collections still REGISTER.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Block run-enrollment when this PC has no WiFi adapter and no default
route. PXE imaging LAN has no DHCP gateway, so towers without WiFi
get stuck in PPKG enrollment (AAD + Intune endpoints unreachable)
and require a re-image. Recurring failure mode observed 2026-05-05.
Tech-facing R/X retry+abort prompt walks them through plugging into
a corp wall jack.
Replace plain post-PPKG reboot with handoff to Monitor-IntuneProgress
-PostPpkg: cancel the pending shutdown timer, run a 180s settle so
MDM can push the baseline policy, render live status during settle,
then issue a clean reboot. The persistent @logon sync_intune task
resumes tracking on the next boot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Filter by PhysicalMediaType + HardwareInterface instead of
InterfaceDescription regex. Name/description varies per vendor
(Realtek Gaming GbE, Intel I219-V, etc.) so a name-only filter
missed adapters on some hardware. Keep an InterfaceDescription
negative guard for drivers that mis-report PhysicalMediaType.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause of fleet not reporting: long-running entries (UDC's
WaitTimeoutSec=120) keep the dispatcher CPU-busy with no SMB traffic
to W:. SMB server times out the idle session. W: stays as a "mapped"
drive letter on the client but Path operations against it fail with
weird errors (e.g. "Cannot bind argument to parameter 'Path' because
it is null" via downstream Join-Path / Test-Path null cascades).
Fix:
- Re-attach W: at the top of the status write-back block (cheap; if
still alive net use returns 'already mapped'; if dead, freshly
remounts).
- Null-guard $hostname (fall back to 'UNKNOWN') and explicitly throw
if $driveLetter is unset (catch surfaces a clear error in log).
- Pair with UDC manifest WaitTimeoutSec reduction 120 -> 60 on the v2
share to limit how long the SMB stays idle in the first place.
Surfaced via blah*.txt log captures from 3 deployed bays (3105, 3115,
3116, 3118 era) - all showed "Status write-back failed: Cannot bind
argument to parameter 'Path' because it is null" while everything
upstream (manifest entries) ran clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
machine-number.txt holds the imaging-time MN. PCs imaged with
placeholder 9999 (tech intends to flip via Set-MachineNumber later)
keep 9999 in that file even after Update-MachineNumber writes the
real MN to HKLM:\...\Dnc\General\MachineNo. Status.json was reporting
9999 across the fleet because of this.
Now reads DNC reg first; only falls back to machine-number.txt if reg
is missing or also 9999. Existing convergence-check.txt unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds machineNumber field to status.json (read from C:\Enrollment\machine-number.txt).
Bumps enforcerVersion to '2.5' so check-fleet-convergence.txt's column
flips when fleet picks up the self-update. Pairs with the Install-FromManifest
v2.5 (WaitTimeoutSec) so both bumps land together.
Convergence-check txt updated to display the new MN column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug in lib v2.5 introduced 2026-05-04: WaitTimeoutSec branch called the
non-existent function Test-Installed. Should be Test-AppInstalled (the
actual function name elsewhere in the same file).
Caught when J test ran and surfaced "The term 'Test-Installed' is not
recognized" after the WaitTimeoutSec=120 fired on UDC. UDC entry then
exited -1 (Process.Start failed catch block) instead of 0 (success
post-detection-recheck). Functional impact was containable - dispatcher
moved on to next entries - but UDC always reported FAILED.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per user 2026-05-04: the 31-char WJFMS3.APPS.WLM.GEAEROSPACE.NET
empirically did NOT work end-to-end, contradicting the earlier "just
tested it works" report (which turned out to confirm a different
codepath, not eDNC FMS prescan).
Re-aligns with the original Ghidra-derived 20-byte buffer constraint:
WJFMS3.AE.GE.COM = 16 chars + null = 17 bytes, fits cleanly in
CPreScan + CDoPersonnel's local_28[20] / size=0x14 RegQueryValueExA
buffer. Anything > 19 chars hits ERROR_MORE_DATA and falls into the
"Cannot Read field" failure path.
FMSHostSecondary unchanged at 10.233.112.158 (IP literal). v2 mirror
also updated: gea-shopfloor-{collections,nocollections} drift-catcher
RegValue, common/Set-FmsHostsEntry.ps1 script, common/manifest.json
hosts pin entry name. Re-run SYNC-TO-PROD.md robocopy to push.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UDC_Setup.exe is a WiX Burn bootstrapper that installs the underlying
MSI cleanly with /quiet but the wrapper process never exits - waits on
a bundled child service that doesn't return control. Empirically:
DisplayVersion=1.0.34 + UDC.exe present in C:\Program Files\UDC after
~30s, but Process.WaitForExit blocks indefinitely (>5 min observed).
EXE handler now honors optional WaitTimeoutSec on the manifest entry.
After timeout, kills the wrapper, re-runs Test-Installed; if detection
passes, treats as success (rc 0); if fail, surfaces as -2 (distinct
from -1 Process.Start failure). Default unset = old behavior (block
forever via WaitForExit) so existing entries unaffected.
Pairs with UDC manifest entry update on the v2 share:
InstallArgs: "West Jefferson" 9999 -> /quiet /norestart
+ WaitTimeoutSec: 120
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per user empirical confirmation 2026-05-04: the 31-char FQDN works on a
freshly-imaged production PC. Earlier failed attempts that surfaced as
"buffer too small" symptoms were actually a typo (WILM vs WLM) reaching
a non-resolvable hostname rather than a true reg-read buffer overflow.
The 20-byte buffer cap I observed in the Ghidra decomp of CPreScan +
CDoPersonnel may apply to a different code path, or the deployed prod
binary has a larger buffer than the VM-extracted MSI (DncMain.exe SHA
F8BFC2574288FD08ACBE6BC0D97E80A9C6EF57E2EB222F0CE752C2DC15F12223)
copy. Empirical evidence wins.
Sweep covers all 147 per-bay .reg files. FMSHostSecondary unchanged at
10.233.112.158 (IP literal still bypasses gethostbyname via inet_addr).
v2 share local mirror at
/home/camp/pxe-images/tsgwp00525-v2/shared/dt/shopfloor/main/ntlars-backups/
also updated (not in repo). Plus the FMS Primary host drift-catcher
RegValue in gea-shopfloor-{collections,nocollections}/manifest.json
and the hosts pin in common/scripts/Set-FmsHostsEntry.ps1 - all on the
v2 mirror, propagate to prod via SYNC-TO-PROD.md robocopy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hardcoded version string surfaced in _outputs/logs/<host>/status.json
that the fleet check-in dashboard reads. Bump aligned with
Install-FromManifest lib v2.4 (PCTypes alias map + network-share EXE
staging). Lets the convergence-check oneliner distinguish PCs that
have picked up the post-rename dispatcher from those still on 2.0.
When pushed to share + the self-update common/manifest.json entry's
DetectionValue is bumped to the new SHA256 (commit notes record the
hash but the manifest itself lives on the v2 share, not in this repo
by design), every fleet PC's next cycle re-fires the self-update,
copies new bytes locally, the cycle after that writes status.json
with enforcerVersion=2.4. Fully visible in the dashboard read.
new GE-Enforce.ps1 SHA256:
C8C14CFCE539ACDC6D16B31D2C456A5239516BDFA1EBFC820794A2D58BA7D9AC
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pairs with the rename reorg's other alias maps (Install-FromManifest,
GE-Enforce, Get-PCProfile, verify-state). Fleet PCs whose pc-type.txt
becomes a new gea-shopfloor-* string still match legacy preinstall.json
PCTypes filters like ["Standard"], ["CMM"]. Same map shape as the
others - extract to a shared lib later if drift becomes a problem.
Without this, a freshly-imaged PC writing pc-type.txt =
gea-shopfloor-collections would skip every preinstall.json entry whose
PCTypes is Standard/CMM/etc - imaging chain installs nothing past common
apps with PCTypes=*.
Deployed to PXE server at /srv/samba/enrollment/shopfloor-setup/Shopfloor/
2026-05-04 alongside the rest of the renamed shopfloor-setup tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundles drift left uncommitted from prior sessions and the UDC matrix
verify entry added today.
Drift items (all per session-progress.md, completed in earlier sessions
but never staged):
- playbook/check-bios.cmd (deleted, moved to BIOS/check-bios.cmd)
- playbook/migrate-to-wifi.ps1 (made no-op 2026-04-24 after the dnsmasq
no-gateway fix removed the wired-NIC race that motivated it)
- playbook/preinstall/oracle/Install-Oracle11r2.cmd (post-OUI .ora copy
added 2026-04-24)
- playbook/preinstall/oracle/tnsnames.ora (live tnsnames, 469 KB,
deployed alongside the wrapper 2026-04-24)
- playbook/pxe_server_setup.yml (dnsmasq dhcp-option=3,6 commented,
Oracle .ora deploy task added 2026-04-24)
- playbook/shopfloor-setup/BIOS/{check-bios.cmd, models.txt} (BIOS
detection refinements)
- playbook/shopfloor-setup/Shopfloor/Force-Lockdown.bat
- playbook/shopfloor-setup/Shopfloor/Monitor-IntuneProgress.ps1
- playbook/shopfloor-setup/Shopfloor/SetShopfloorAutoLogon.bat (new)
- playbook/shopfloor-setup/Shopfloor/09-Install-PrinterInstallerMap.ps1
(new, places PrinterInstallerMap.exe + Public Desktop shortcut at
imaging time; manifest entry self-heals on tamper)
- playbook/shopfloor-setup/Shopfloor/lib/Show-IntuneDeviceQR.ps1 (new,
standalone QR rendering for site that wanted just that piece)
- playbook/shopfloor-setup/gea-shopfloor-collections/{Install-eMxInfo.cmd.template,
Restore-UDCData.ps1} (these were uncommitted in pre-rename Standard/;
git mv didn't catch them because they were untracked at the time)
- docs/shopfloor-machine-imaging-guide.md (operator-facing how-to)
Matrix:
- common.test/matrix.json: add UDC verify entry to gea-shopfloor-collections
row. Surfaces UDC silent-install issue (item H pending) instead of
letting it pass silently.
.gitignore:
- PrinterInstallerMap.exe (142 MB) excluded. Track via LFS or stage on
PXE server only - too big for regular git history. Untouched on disk
so existing local copy still works.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lib v2.4. Process.Start of an EXE that lives on a network share fails
with "Access is denied" when the dispatcher runs as SYSTEM, even when
the share is properly mounted via cmdkey + net use. Empirically
confirmed 2026-05-02 with UDC_Setup.exe via qga.
Fix: when the resolved EXE path is on a UNC or PSDrive-with-DisplayRoot
mount, copy the file into a per-cycle temp dir under $env:TEMP and run
from there. Cleanup happens in finally regardless of run outcome.
Cost is one transit per fire, which is rare in practice because most
EXE entries skip on subsequent cycles via DetectionMethod.
Validated on win11 VM with UDC_Setup.exe: dispatcher previously
returned blank exit code with "Access is denied" in stderr; now logs
"staged network EXE -> C:\WINDOWS\TEMP\ge-enforce-exe-..." and the
process runs to Exit 0 in ~18 seconds. UDC's separate "exit 0 without
actually installing" issue is a wrong-silent-flag in InstallArgs, not
this dispatcher fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pairs with Phase 1+2 from earlier (alias maps in Install-FromManifest,
GE-Enforce, Get-PCProfile, verify-state). See project-shopfloor-rename-reorg
memory for the plan.
Phase 3 (repo + paths):
- git mv per-PC-type dirs to gea-shopfloor-* names:
Standard -> gea-shopfloor-collections
CMM -> gea-shopfloor-cmm
Keyence -> gea-shopfloor-keyence
Genspect -> gea-shopfloor-genspect
WaxAndTrace -> gea-shopfloor-waxtrace
Display -> gea-shopfloor-display
Lab -> gea-shopfloor-common (folded; Timeclock+Lab merge)
- New gea-shopfloor-nocollections/ (clone of collections sans UDC scripts).
- New gea-shopfloor-heattreat/ (placeholder, README only).
- Move Standard/ntlars-backups/ -> _ntlars-backups/ (per-MN, not per-type).
- Run-ShopfloorSetup.ps1: Resolve-PCTypeDir helper walks alias group when
the on-disk dir for the current pcType is missing. Set-MachineNumber
helper-copy gated on collections|nocollections|legacy Standard-Machine.
- Update-MachineNumber.ps1: pcProfiles lookups try gea-shopfloor-collections
first, fall back to legacy Standard-Machine. PowerShell 5.1 compatible
(no null-coalesce).
Phase 4 (startnet.cmd menu):
- Choice 3 "GEA Shopfloor" now drills into a 9-item sub-menu instead of
going straight to enrollment. Sub-cats:
1. Machine with Collections -> gea-shopfloor-collections
2. Machine without Collections -> gea-shopfloor-nocollections
3. Common (Timeclock, Lab) -> gea-shopfloor-common
4. Keyence -> gea-shopfloor-keyence
5. CMM -> gea-shopfloor-cmm
6. Genspect -> gea-shopfloor-genspect
7. Heattreat -> gea-shopfloor-heattreat
8. Wax and Trace -> gea-shopfloor-waxtrace
9. Display -> gea-shopfloor-display
- Office menu (existing 6-option) follows for every sub-cat.
- Machine number prompt only for collections + nocollections.
- pc-subtype.txt + display-type.txt no longer written. PCTYPE is a
single full string (gea-shopfloor-*); subtype-aware code paths fall
back to empty and resolve via the alias map.
- CMM bootstrap stage gate switched from "%PCTYPE%"=="CMM" to
"%PCTYPE%"=="gea-shopfloor-cmm".
Test harness:
- B-enforce/run.sh PCSUBTYPE default changed from "Machine" to "" so
single-arg invocation matches the new single-string scheme. Two-arg
legacy form ("Standard Machine") still works via aliasing.
- B-enforce/tamper.ps1 alias-aware Test-MatrixEntryMatches mirroring
verify-state.ps1.
Smoke-tested on win11 VM as SYSTEM via qga: B-enforce harness 5-phase
cycle (stage / baseline / tamper / heal / idempotent) passes 10/10
with PCType=gea-shopfloor-collections AND with legacy "Standard Machine"
two-arg form.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 7 of the gea-shopfloor-* rename. SCOPE.md keeps legacy names
("Standard-Machine", "CMM", "Lab", etc.) as primary keys for now;
new names accepted via alias maps in 4 places: Install-FromManifest,
GE-Enforce, Get-PCProfile, verify-state. See project-shopfloor-rename-reorg
memory for full plan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 5 + 6 of the gea-shopfloor-* rename.
Get-PCProfile.ps1: when the legacy profileKey ("Standard-Machine",
"CMM", etc.) is missing from siteConfig.pcProfiles, walks the alias
group and returns the first matching new key ("gea-shopfloor-collections",
"gea-shopfloor-cmm", etc.). Vice versa: a fleet PC writing the new
string finds its profile under the old key. Same alias map shape as
GE-Enforce + Install-FromManifest, kept in sync manually for now -
extract to shared file later if drift becomes a problem.
matrix.json: adds 3 new rows for gea-shopfloor-nocollections,
gea-shopfloor-common (Timeclock+Lab merge), gea-shopfloor-heattreat
(placeholder). Existing rows for legacy names retained; the new
verify-state alias resolution lets either be requested.
verify-state.ps1: Test-MatrixEntryMatches walks the alias map so
harness invocation with "Standard Machine" or "gea-shopfloor-collections"
both resolve to the same matrix row.
Smoke-tested via qga-as-SYSTEM on win11: legacy Standard/Machine,
new gea-shopfloor-collections, and new gea-shopfloor-nocollections
all return 10/10 pass against current VM state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 of the gea-shopfloor-* rename. Pairs with the v2 share manifest
dir renames done in /home/camp/pxe-images/tsgwp00525-v2/ this session
(local-only, syncs to prod separately):
standard-machine -> gea-shopfloor-collections
cmm -> gea-shopfloor-cmm
keyence -> gea-shopfloor-keyence
genspect -> gea-shopfloor-genspect
waxandtrace -> gea-shopfloor-waxtrace
display -> gea-shopfloor-display
lab -> merged into gea-shopfloor-common
(new) -> gea-shopfloor-nocollections (clone of collections w/o UDC)
(new) -> gea-shopfloor-heattreat (placeholder)
(new) -> gea-shopfloor-common (Timeclock + Lab merge)
GE-Enforce now walks an alias group when the constructed dir name has
no manifest.json. Fleet PCs whose pc-type.txt still says "Standard" /
sub "Machine" continue to find their manifest at the new
gea-shopfloor-collections location, so the rename is invisible to them.
After Phase 4 (startnet.cmd) lands and freshly-imaged PCs write the new
strings directly, the alias resolution still works for both forms.
Smoke-tested on win11 VM as SYSTEM via qga: legacy Standard/Machine
and new gea-shopfloor-collections both reach the same manifest, fire
the same entries, complete cleanly.
Phases 3+4 (repo folder renames + startnet.cmd menu) deferred per
project-shopfloor-rename-reorg memory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of the gea-shopfloor-* rename per project-shopfloor-rename-reorg.
Manifests can use either old names (Standard, Standard-Machine, CMM,
Keyence, etc.) or new names (gea-shopfloor-collections,
gea-shopfloor-cmm, gea-shopfloor-keyence, etc.) interchangeably.
Equivalence sets defined inline. Each set is a list of names that all
match the same identity. The match logic resolves the current PC's
identity AND each PCTypes entry into their alias sets, then matches
if the sets intersect.
Standard maps to all three new shopfloor variants (collections,
nocollections, common) so an existing PCTypes=['Standard'] manifest
entry still applies when PC pc-type.txt becomes any of the three.
Standard-Machine maps to (collections, nocollections) only since
Timeclock subtype is now collapsed under common.
Smoke-tested on win11 VM as SYSTEM via qga: dispatcher run with
PCType='gea-shopfloor-collections' against the existing common
manifest (Standard-only PCTypes filters) fires Oracle / FMS hosts pin
correctly. Same run with PCType='Standard' PCSubType='Machine' fires
identically.
Phases 3+4 (repo folder renames + startnet.cmd menu reorg) deferred to
the next session - high breakage risk, must ship atomically.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds rows for Standard-Timeclock, CMM, Keyence, Lab, WaxAndTrace,
Genspect, Display, Shopfloor alongside the existing Standard-Machine.
Per-type apps verified against the corresponding v2 manifest's detection
methods (PC-DMIS 2016/2019R2/Protect Viewer/CLM/goCMM for CMM;
VR-6000/USB driver for Keyence; kiosk shortcut for Display).
Common app list deduped via "$ref": "common.<key>" pattern. Verifier
resolves refs into the per-type apps array at runtime so each row stays
short and PCTypes-filter-aware (Lab + Display + Shopfloor get fewer
common apps because the manifest's PCTypes filter excludes them from
FMS hosts pin / Oracle / OpenText respectively).
verify-state.ps1 changes:
- $ref resolution against the matrix.common namespace
- Registry method now permits no DetectionName (key-existence only,
e.g. Protect Viewer)
- New PnpUtilGrep method for INF-driver checks (Keyence USB driver)
Smoke-verified end-to-end on the win11 VM as SYSTEM via qga - 60 checks
across 9 PC types. Type-specific failures (5 CMM, 2 Keyence, 1 Display)
correctly surface "no payload staged" rather than masking it as pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>