G1.4: ExtractPackageStep — pre-rewriter .nupkg/.zip extraction
Summary
Closes the "extract before rewriter" gap. When the wire literal Squid.Action.Package.OriginalPath points at a .nupkg / .zip, the new step extracts it into context.WorkingDirectory BEFORE the rewriter pipeline runs. Standalone-script deploys (no package) keep their existing UX — the step is a no-op when the literal is unset.
Pipeline: LoadVariablesFromFiles → ExtractPackage → SubstituteInFiles → ConfigurationTransforms → JsonConfigVariables → WriteBootstrapped → Execute.
Hostile-archive safety
| Defence | What it catches | Test |
|---|---|---|
| Zip-slip | Entry with .. resolving outside destination |
Extract_ZipSlipEntry_Rejected_DestinationUntouched + 2 variants |
| Absolute path |
/etc/passwd / C:\Windows\...
|
Extract_AbsolutePathEntry_Rejected |
| Per-entry size cap | Single huge file |
Extract_EntryExceedsPerEntryCap_Rejected — reuses shared T3 env var |
| Total size cap (zip-bomb) | Many entries summing to multi-GB |
Extract_TotalSizeExceedsBombCap_RejectedMidStream — 10x per-entry |
| Fail-closed | Any single violation aborts whole extract | All above — never partial extract |
| Overwrite | Re-deploys land cleanly | Extract_OverwritesExistingFiles_SecondExtractWins |
What's in the box
| Layer | File |
|---|---|
| Pure-function extractor |
src/Squid.Calamari/Commands/Package/ZipExtractor.cs (new) |
| Pipeline step + wire literal |
src/Squid.Calamari/Commands/Package/ExtractPackageStep.cs (new) |
| Pipeline wiring | src/Squid.Calamari/Commands/RunScriptCommand.cs |
| ZipExtractor tests (11) |
tests/Squid.Calamari.Tests/Calamari/Commands/Package/ZipExtractorTests.cs (new) |
| Step tests (12) |
tests/Squid.Calamari.Tests/Calamari/Commands/Package/ExtractPackageStepTests.cs (new) |
Test plan
-
Squid.Calamari.Tests: 300/300 (was 277; +23) -
Solution build: 0 errors -
Wire literal Squid.Action.Package.OriginalPathpinned (Rule 8 drift detector) -
Per-entry size cap reuses T3 env var SQUID_CALAMARI_REWRITER_MAX_FILE_SIZE_MB -
Standalone-script deploys (no OriginalPathset) are zero-impact — IsEnabled short-circuits -
Staging: actual .nupkgdeploy withappsettings.jsonrequiring token substitution post-extract
Non-breaking guarantee
| Surface | Status |
|---|---|
| Existing wire literals | Unchanged |
Standalone script deploys (no OriginalPath) |
Zero impact — step skips |
| IIS PS1 in-script extraction path | Unchanged — Squid Calamari ExtractPackageStep doesn't replace it; the PS1 still handles its own extract until that's migrated separately |
| SquidWeb | Unchanged — OriginalPath is server-emitted, not operator-facing |
The IIS handler's PS1 still does its own in-script extraction; this PR adds the agent-pipeline extraction surface that future generic handlers (RunScript with package, Docker, nginx, etc.) will emit OriginalPath to drive.