Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

11KB

WSC-MVC — Architecture (as implemented through M2)

Request flow

GET /hello
  -> IIS URL Rewrite rule "WscMvc-Hello" (^hello/?$)
  -> /Default.asp?route=/hello
  -> Server.CreateObject("WscMvc.RequestContext"); ctx.Initialize path, httpMethod, logDir
  -> Server.CreateObject("WscMvc.Application")
  -> Application.Run(ctx, statusLine, contentType, body)         [Framework/Application.wsc]
       -> CreateObject("WscMvc.HomeController")
       -> HomeController.Hello(body)                             [Controllers/HomeController.wsc]
       -> LogOutcome ctx, statusLine  (best-effort append to logs/app.log)
  -> Default.asp sets Response.Status/ContentType, writes body

Component boundary contract

  • Default.asp is the only file that touches ASP intrinsic objects (Request, Response, Server). It contains no business logic — only reading the route query parameter and REQUEST_METHOD, resolving logs/'s physical path via Server.MapPath, invoking WscMvc.RequestContext and WscMvc.Application, and writing the response.
  • Framework/RequestContext.wsc, Framework/Application.wsc, and Controllers/HomeController.wsc never reference Request/Response/Server/Session. All data crosses the ASP-to-WSC and WSC-to-WSC boundaries as either VBScript scalars (strings) or our own RequestContext COM object (not an ASP intrinsic) — never an ASP host object. See docs/DECISIONS.md for why the original SPEC §15 open question about passing ASP intrinsics into a WSC never needed a direct experiment: the architecture never crosses that boundary by design.
  • WSC public members are exposed as plain <method> entries backed by ordinary Sub/Function procedures, never <property>/Property Get. VBScript's Property Get/Let/Set requires a Class...End Class block and cannot appear at a WSC's top-level script scope — confirmed experimentally (see docs/DECISIONS.md), not assumed from general WSC documentation.
  • Routing in M1/M2 is still a single hardcoded If path = "/hello" check inside Application.Run. This is intentionally minimal and will be replaced by the explicit allowlisted route table in M3 (Router.wsc); do not extend it ad hoc before that milestone.
  • Application.Run is the single central point that decides expected (404, ordinary control flow) vs. unexpected (COM/method failure, 500) outcomes, and the single point that logs every outcome. Default.asp still independently guards its own three sequential calls (RequestContext creation, Initialize, Application creation, Run) since a WSC failing to even instantiate happens outside Application.Run's reach.

Per-request lifetime and diagnostics

  • RequestContext and Application are both created fresh per request via Server.CreateObject in Default.asp and dropped (Set ... = Nothing) at the end of the page; neither is ever cached in ASP Session/Application scope, per SPEC §10.
  • RequestContext.Initialize generates a correlation id from Fix(Timer) plus Scripting.FileSystemObject.GetTempName() — not Randomize/Rnd(), which was tried first and shown experimentally to collide when two contexts are created within the same clock tick (see docs/DECISIONS.md).
  • Application.LogOutcome appends one line per request (timestamp, correlation id, method, path, status line, elapsed ms) to logs/app.log, guarded end-to-end by On Error Resume Next so a logging failure can never affect the HTTP response. Concurrent writers are serialized with a manual lock-file mutex (logs/app.log.lock, via CreateTextFile(..., OverwriteExisting:=False)) since no ASP intrinsic locking primitive (Application.Lock) is available to code that must not reference ASP intrinsics. The retry budget is deliberately short (see docs/DECISIONS.md): logging is explicitly best-effort and may drop lines under heavy concurrency without affecting correctness of the response.

Test harness: GET /self-test — its own app, same shared framework

Controllers/SelfTestController.wsc (WscMvc.SelfTestController) exposes the same checks as tests/Test-Components.vbs, but callable over plain HTTP and returning JSON — no PowerShell, cscript, or SSH access to the VM required. Application.Run routes path = "/self-test" to it.

This lives on its own IIS site (test-app/), not the production site. Changed 2026-09-19 at Daniel's explicit direction: a diagnostics endpoint permanently reachable on the same site/port as real traffic was the wrong shape. test-app/public/ is a second, separate IIS site's physical path (own site name, own app pool, own port, own logs/), whose web.config routes /self-test and nothing else. The production site's web.config no longer has a /self-test rewrite rule at all — GET /self-test against the production site now returns a plain 404 (nothing rewrites that path to Default.asp).

“Own app, same framework” is achieved without duplicating any business logic: Application.wsc's Run method still has (and needs) its path = "/self-test" branch — that's shared framework code, registered once globally via COM, used by whichever site's web.config chooses to route a URL to it. The only genuinely duplicated file is Default.asp itself (also present at test-app/public/Default.asp), and only because IIS requires each site to have its own physical files — the bootstrap logic in that file is fully generic (doesn't hardcode routes or reference which site is calling it), so the two copies are intentionally byte-identical. If Default.asp's bootstrap logic ever needs to change, change both copies the same way (see the comment at the top of each file).

The HTTP status is always 200 if the self-test mechanism itself ran (a broken SelfTestController/Application still degrades to the normal 500 path via the same central error handling); the JSON body's top-level "ok" field and each check's "pass" field carry the actual test results — conventional health-check design (5xx is reserved for the diagnostics mechanism being broken, not for a failed assertion inside it).

curl http://100.127.62.31:8091/self-test
{"ok":true,"checks":[{"name":"request_context_contract","pass":true,"detail":""}, ...]}

tests/run-self-test.sh wraps this in curl + python3 -m json.tool, runnable from any CLI (verified working directly from the Linux OpenClaw host, not just from the VM) — point it at the test-app site's URL. tests/Test-Http.ps1 tests the production site only (/hello, and confirms /self-test is genuinely unreachable there).

SelfTestController's error details intentionally include raw Err.Description text — unlike every other route, which must never leak internals to an arbitrary caller, this route's entire purpose is diagnostics. It is a dev/test-milestone tool; reconsider whether the test-app site should be gated or removed before any production deployment during the M6 hardening pass.

COM identity

Component ProgID CLSID
Framework/RequestContext.wsc WscMvc.RequestContext {1C36FA55-34DF-4974-94B9-D657389362B2}
Framework/Application.wsc WscMvc.Application {851C7763-1638-42FE-A166-BF3DD3A96A88}
Controllers/HomeController.wsc WscMvc.HomeController {87488446-60BE-4068-8368-0B709BB68F3F}
Controllers/SelfTestController.wsc WscMvc.SelfTestController {D2634944-4646-4C55-956E-4C05E7E10904}

CLSIDs are fixed at creation and must never be recycled for a different component (AGENTS.md). Application's public Run signature changed between M1 and M2 (added a leading ctx parameter) under the same CLSID; acceptable because this is active pre-release (v0.1) development with exactly one caller (Default.asp, updated in lockstep) — not a claim that live interface changes are safe for a published/external client.

IIS sites (test host: win2025test, 100.127.62.31)

Two separate IIS sites now, both set up/reconciled by the same tools/Setup-Site.ps1 (parameterized by name/pool/path/port; idempotent, safe to re-run):

Production Test app
Site name WscMvc WscMvcTests
Port *:8090 *:8091
Physical path C:\Projects\wsc-mvc\public C:\Projects\wsc-mvc\test-app\public
App pool WscMvc WscMvcTests
Routes /hello /self-test
Logs C:\Projects\wsc-mvc\logs\app.log C:\Projects\wsc-mvc\test-app\logs\app.log

Both are 64-bit, no managed code, anonymous auth identity IUSR. Each site's public/ contains only Default.asp and web.config; every other directory (Framework/, Controllers/, tests/, tools/, docs/, both logs/ folders) is a sibling of whichever public/ is actually served, outside both sites’ physical paths entirely. Changed 2026-09-19 at Daniel's explicit direction (see docs/DECISIONS.md); previously there was one site with the whole project directory as its root and those folders hidden via hiddenSegments, and /self-test was a route on that same site.

  • Each site's web.config keeps only a .wsc/.vbs/.ps1/.md extension denylist as defense-in-depth (in case a stray file ever lands directly in its public/); no hiddenSegments are needed since the directories they used to hide no longer exist under either served root at all.
  • enableParentPaths=true is set for both sites (scoped via appcmd ... /commit:apphost, a separate <location> block per site in applicationHost.config — not a machine-wide unlock of the locked system.webServer/asp section) so each site's Default.asp can Server.MapPath("../logs") to reach its own logs/. This setting affects only server-side script MapPath/#include resolution, not client-supplied URL paths — IIS's own URL normalization independently rejects ..-traversal in an incoming request regardless of this setting; verified with a direct request attempt, see docs/DECISIONS.md.
  • Each site's logs/ has an explicit, scoped icacls ... /grant "IIS_IUSRS:(OI)(CI)M" (now automated by tools/Setup-Site.ps1, previously a manual one-off command) so classic ASP (impersonating IUSR for anonymous requests) can write app.log. No other project directory grants IUSR/IIS_IUSRS write access — verified with a recursive icacls /T scan, see docs/DECISIONS.md.
  • Default document is Default.asp on both sites; each site's one route is served via a rewrite rule, not the default document.

Deferred to later milestones (do not implement early)

Per SPEC §3 non-goals and IMPLEMENTATION_PLAN M3+: explicit route table, HTML views/templates, ADODB, auth. A more robust logging mechanism (if complete coverage under concurrent load is ever required) is deferred to M6 — see the concurrency finding in docs/DECISIONS.md.

Powered by TurnKey Linux.