Skip to content

feat: encode the PII script capability in the CgScript file name - #159

Merged
davhdavh merged 10 commits into
mainfrom
feat/cgscript-pii-filename-marker
Sep 18, 2026
Merged

davhdavh merged 10 commits into
mainfrom
feat/cgscript-pii-filename-marker

Conversation

@davhdavh

@davhdavh davhdavh commented Sep 18, 2026 •

Copy link
Copy Markdown
Member

feat: encode the PII script capability in the CgScript file name

What this does

A .cgs file can declare that it may read personal data, in its own file name, using the same metadata channel that already carries impersonation and public access:

name[@<userId>[.pii][.public]].cgs

The deployer parses that marker with one shared strict parser and sends it to the site as canAccessPII in the existing UpdateScripts payload - a field the site already models (QuestionnaireScript.CanAccessPII), enforces (IPiiAccessPolicy.CanIssueAuthenticationPii) and hashes.

Valid: Name.cgs, Name@0.cgs, Name@123.cgs, Name@123.pii.cgs, Name@123.public.cgs, Name@123.pii.public.cgs.
.pii sits closest to the id it scopes; .public is the outer modifier.

Why

The file name was already the only channel that carried a script's security metadata, and the site contract already had the field - nothing on the file side could express it. This closes that gap without adding a configuration surface.

Accepted behaviour changes

Fail-loud replaces silent name-folding, so these shapes are now errors naming the file and the offending suffix: unknown token, wrong marker order (.public.pii), duplicate markers, empty token, non-numeric / negative / out-of-range ids (max 2147483647 - the site casts to int), a marker combined with @0, a .pii/.public tail with no @, a leaf without .cgs, and control characters or " in a name.

Consequently: any @ in the leaf is a metadata boundary, so e.g. user@domain.cgs must be renamed; and Name@123.pii.cgs now registers as script Name instead of the literal Name@123.pii - a one-time rename, with a duplicate-identity error naming both files if Name.cgs also exists.

These are enumerated in the generator commit message so the generated release note carries them; the README stays terse and points at the ScriptFromFileOnDisk documentation for the grammar details.

No extra permission on the deployer side

The token request is unchanged (scope=scriptdeployment:w, asserted by the payload tests). A site that predates canAccessPII silently ignores the member, so deploy the site first; the impersonated user must satisfy the site's workflow-PII policy (TemplateEligible && !BuiltInAdmin && !AdminGroup).

Analyzer consumers

CGS028 (Error) makes a malformed script file name fail the build instead of emitting a wrapper that calls a script name the deployer would never register. The rule is tracked in AnalyzerReleases.Shipped.md alongside the other shipped rules.

Verification

  • New Catglobe.CgScript.Deployment.Tests (net10.0/xunit): 81 passed / 0 failed / 0 skipped - the filename truth table (62 rows), provider identity rules, the captured UpdateScripts bodies, and a Roslyn-driven generator parity test.
  • Regression: Catglobe.CgScript.EditorSupport.Lsp.Tests 344 passed / 0 failed / 0 skipped.
  • Real surface: the demo builds with 0 errors and its @115.public script still generates the same wrapper; a real AddCgScriptDeployment end-to-end run against a local stub endpoint captured "canAccessPII":true for a .pii script with no pii scope on the token request; a real MSBuild run with EmitCompilerGeneratedFiles emitted Execute("Live", ...) for Live@7.pii.cgs.
  • Independent gates: plan compliance, code quality (47 falsification probes), real manual QA and scope fidelity all APPROVE, plus a final gate review APPROVE over the rebased head (six adversarial parser probes, both suites re-run, security shape confirmed).
  • The generator project still builds with only the two pre-existing RS2007 warnings.

Plan: .omo/plans/cgscript-pii-filename-marker.md

Parses name[@<userId>[.pii][.public]].cgs from the leaf only; canonical .pii-then-.public order; ids 0..2147483647; fail-loud errors naming the file, the offending suffix and the expected shape.
Grammar, valid and rejected shapes, the four accepted behaviour changes including the one-time rename, the site PII policy predicate, and that the deployer still requests only scriptdeployment:w.
Default interface member is fail-closed (false) so custom providers keep compiling; ScriptFromFileOnDisk now delegates the whole filename grammar to ScriptFileNameParser; the directory provider reports duplicate script identities with both file names instead of a raw dictionary error.
…med names

The analyzer source-links ScriptFileNameParser and passes the leaf with its .cgs
extension, so wrapper names always match what the deployer registers. Malformed
names now fail the build with CGS028 (Error) instead of silently emitting a
wrapper for a wrong script name that would never exist on the site.

Accepted naming breaks shipped with this change, documented in the README:
- a malformed tail after the last @ is an error where it used to be folded into
  the script name (Name@123.publc.cgs, Name@abc.cgs, Name@123.public.pii.cgs);
- a trailing .pii/.public with no @ is an error (Name.pii.cgs, Name.public.cgs);
- .pii/.public combined with @0 is an error; such a file used to register and
  then fail on the site with 412/403;
- an existing Name@123.pii.cgs now registers as script 'Name' instead of the
  literal 'Name@123.pii' - a one-time rename; the deployer reports a duplicate
  script identity naming both files if Name.cgs also exists.
The deployer now forwards the PII classification parsed from the file name to the site. The token request still asks only for scriptdeployment:w: the site infers the capability from that scope, and its deploy gate checks the impersonated user instead of a pii scope. Capture-handler tests assert the raw request bodies, the call order, and the zero-request failure path for a malformed file name.
The previous wording implied @-containing names keep deploying; they do not. Also adds the README example Report@123.publc.cgs as a literal test row so the documented examples and the test data agree in both directions.

Plan: .omo/plans/cgscript-pii-filename-marker.md
The rule row belongs in AnalyzerReleases.Shipped.md with the other shipped rules; Unshipped keeps its empty table. The README section shrinks to the grammar line, what the markers mean, the fail-loud behaviour and the pointer to the ScriptFromFileOnDisk documentation, which holds the details. The skill template keeps the two marker rows without the added rules paragraph.
Development-run scripts treat .pii and .public as ordinary names; the sentence states the rule without referring to past behaviour, keeping the security section terse.
@github-actions

This comment has been minimized.

The new test project now emits coverlet cobertura like the LSP test project, into its own subdirectory of test-results-output: both projects used coverlet default file name in the same directory, so a solution-wide run (what CI does) silently kept only one report. The workflow glob is recursive, so both reports now reach ReportGenerator.
@github-actions

Copy link
Copy Markdown

Summary

Line coverage Branch coverage

Assembly Line coverage Branch coverage
Catglobe.CgScript.Common 27.9% 22.7%
Catglobe.CgScript.Deployment 58.8% 31.4%
Catglobe.CgScript.EditorSupport.Lsp 39% 38.8%
Catglobe.CgScript.EditorSupport.Parsing 68.5% 65.7%
Catglobe.CgScript.EditorSupport.SourceGenerator 69.7% 29.3%

@davhdavh
davhdavh merged commit 3d313f0 into main Sep 18, 2026
2 checks passed
@davhdavh
davhdavh deleted the feat/cgscript-pii-filename-marker branch September 18, 2026 08:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant