# WSC-MVC — Decisions and Experiment Log ## M0 — Environment inventory (2026-09-19, win2025test VM, 100.127.62.31) Host: Windows Server 2025 Standard, build 10.0.26100, 64-bit, AMD Ryzen 5 1500X (4c). Verified via SSH (key-based, `Administrator`) + PowerShell 5.1. - IIS: installed (`Web-Server`) with `Web-ASP`, `Web-CGI`, `Web-ISAPI-Ext`, `Web-ISAPI-Filter`, management tools/console/scripting tools all Installed. - URL Rewrite module: present (`%SystemRoot%\system32\inetsrv\rewrite.dll`, registered as `RewriteModule` in global modules). - WebAdministration PowerShell module: available (v1.0.0.0). - WSC/COM runtime: `scrobj.dll` present in both `System32` and `SysWOW64`. - `regsvr32.exe`, `cscript.exe`, `wscript.exe`: present at standard `C:\WINDOWS\system32` paths. - Existing app pools are all 64-bit (`enable32BitAppOnWin64 = False`); no 32-bit requirement observed. WSC-MVC will target 64-bit and re-verify if this changes. - Existing sites: `Default Web Site` (`*:80`), `AspClassicUnifiedFramework` (`*:8080`, unrelated pre-existing project). No port conflict for a new dedicated site on `*:8090`. - Disk: `C:` only (106 GB, ~80 GB free). No `D:`/`E:`/`F:` volumes despite drive letters being visible (0 bytes each — not usable). Project deployed under `C:\Projects\wsc-mvc`. - Firewall: `World Wide Web Services (HTTP Traffic-In)` inbound rule enabled. - HTTP reachability: VM only reachable over the private Tailscale tailnet (100.127.62.31); no public exposure. No blocking gaps found for M0/M1. Proceeding to M1. ## Open question #1 — Can WSC receive ASP intrinsic objects across the IIS/COM boundary? **Decision: not attempted for M1.** Per SPEC and AGENTS.md hard rules, business logic must never reference ASP `Request`/`Response`/`Server`/`Session` anyway, so this is avoided by design rather than tested as a compatibility question. `Application.Run` and `HomeController.Hello` use a **primitive-only, ByRef-out contract**: all parameters are strings (`pathInfo` in; `statusLine`, `contentType`, `body` out via VBScript's default ByRef parameter passing — no `ByVal`/`ByRef` keywords are available in WSC `` XML, so behavior follows the `Sub`/`Function` signature in the script block). `Default.asp` owns all ASP host object access and is the only file that touches `Request`/`Response`/`Server`. This resolves the open question pragmatically: no experiment needed because the architecture never crosses that boundary. Revisit only if a future milestone needs to pass richer data (e.g. multi-value POST bodies) — prefer additional scalar out-params or a `Scripting.Dictionary` (a standard COM component, not an ASP intrinsic) over passing ASP objects into a WSC. ## Registration tooling `regsvr32.exe /s ".wsc"` registers a Windows Script Component directly (scrobj.dll is the underlying handler associated with the `.wsc` extension). Verified working for both `Application.wsc` and `HomeController.wsc` on this host — see `docs/TEST-RESULTS.md`. Both scripts require elevation (registry write); the deployment/registration step is intentionally separate from the request-handling pipeline per SPEC §10 and AGENTS.md. **Finding: `regsvr32 /s /u component.wsc` does not actually unregister.** Verified experimentally on this host (Windows Server 2025, build 26100): after `regsvr32 /s /u` on both components, `regsvr32` reports exit code 0, but `HKLM:\SOFTWARE\Classes\` and `HKLM:\SOFTWARE\Classes\CLSID\` are both still present and functional (confirmed via direct registry inspection and a follow-up `CreateObject` that still succeeded). This reproduced identically via `Start-Process -Wait` and via a direct `cmd`-level `regsvr32 /s /u` call with `%ERRORLEVEL%` checked. This is a real scrobj.dll/WSC limitation, not a script bug — do not trust `regsvr32 /u`'s exit code as evidence of removal for `.wsc` files on this host. **Fix implemented**: `tools/Unregister-Components.ps1` still calls `regsvr32 /s /u` first (harmless best-effort), then explicitly checks whether `HKLM:\SOFTWARE\Classes\` still resolves to this project's known CLSID, and if so removes exactly that `ProgID` key and exactly that `CLSID` key (recursive), nothing else. It refuses (throws) instead of deleting if the registered CLSID under that ProgID doesn't match what this project expects, to guarantee it can never remove a different component's registration that happens to reuse a ProgID string. Full reversibility cycle (register → verify via HTTP+WSH → unregister → verify HTTP 500 + WSH `CreateObject` failure → re-register → verify recovery) was run end-to-end and passed; see `docs/TEST-RESULTS.md`. ## Finding: IIS `httpErrors` substitutes its own error page for remote clients by default With the out-of-the-box `system.webServer/httpErrors` setting (`errorMode="DetailedLocalOnly"`), a request from a **remote** client (e.g. over the tailnet, not literally `localhost` on the IIS box) that hits a 500 gets IIS's own generic friendly-error HTML page, not whatever `Default.asp` wrote to the response body — even though `Default.asp`'s own `On Error Resume Next` / `Err.Number` handling ran correctly and wrote its intended `"Internal Server Error"` / `text/plain` body. Verified by comparing a `curl` from the Linux host (tailnet IP) against a `curl` run locally on the VM (`http://localhost:8090/hello`) with components deliberately unregistered: the local request returned our own exact `Content-Type: text/plain; charset=utf-8` / `Internal Server Error` body; the remote request returned IIS's generic HTML page. Both are HTTP 500 and both satisfy the SPEC §14/§10 requirement (safe failure, no path or exception text leaked to any client) — this is IIS being conservative for non-local requests, not a defect in our error handling. Documented here so a future milestone doesn't mistake this for `Default.asp`'s error branch not firing. Diagnostic note: `system.webServer/asp` is locked (`overrideModeDefault="Deny"`) at the machine level by default, so it cannot be overridden from a site's own `web.config`. Toggling `scriptErrorSentToBrowser` for diagnosis requires `appcmd unlock config /section:system.webServer/asp` first; remember to `appcmd lock config /section:system.webServer/asp` again afterward and to manually strip any `` element that `Set-WebConfigurationProperty` writes into the site's `web.config` before re-locking, or the site's config becomes invalid (locked section referenced from a location that has an explicit override) and every request 500s until the stray element is removed. This happened once during M1 testing on this host and was fixed by editing `web.config` directly; the repository's `web.config` was never affected (this only happened to the deployed copy on the VM). ## Static/source protection approach (M1; superseded, see the `public/`-only webroot entry below) M1's original approach: IIS `requestFiltering/hiddenSegments` (blocks any URL path containing `Framework`, `Controllers`, `tests`, `tools`, `docs` as a path segment, anywhere in the tree) plus `requestFiltering/fileExtensions` denylist for `.wsc`, `.vbs`, `.ps1`, `.md`, with `Default.asp` at the project root (matching the SPEC §5 target tree at the time) rather than a separate `public/` webroot. Verified with `tests/Test-Http.ps1` (direct `.wsc` request must return 404). **Superseded 2026-09-19**: replaced with physical separation (`public/`-only IIS site root) at Daniel's explicit direction — see that entry below for the current approach and rationale. The `fileExtensions` denylist is kept as defense-in-depth; `hiddenSegments` was removed since it no longer protects anything that exists under the served root. ## Site/port allocation New dedicated site `WscMvc`, app pool `WscMvc` (64-bit, no managed code), binding `*:8090`, physical path `C:\Projects\wsc-mvc`. Chosen to avoid the existing `*:80` and `*:8080` bindings already in use on this VM. ## M2 — Lifecycle, central error handling, correlation-safe diagnostics (2026-09-19) Added `Framework/RequestContext.wsc` (`WscMvc.RequestContext`), a per-request, primitive-only data holder (path, HTTP method, a correlation id, a log directory, elapsed-time tracking). `Default.asp` creates and `Initialize`s one per request and passes it into `Application.Run(ctx, ...)` in place of the raw `pathInfo` string from M1. `Application.wsc` centralizes both the expected-vs-unexpected outcome decision (404 is expected control flow; a `CreateObject`/method-call failure is 500) and best-effort logging of every outcome (`logs/app.log`: timestamp, correlation id, method, path, status line, elapsed ms) in one place (`LogOutcome`), per SPEC §10/§13's M2 requirement. `logs/` was added to `web.config`'s `hiddenSegments` so it is never HTTP-reachable. Three real defects were found and fixed while building this, all confirmed experimentally rather than assumed: **1. `Property Get/Let/Set` cannot be used at the top level of a WSC `