Skip to content

feat(runtime): add HostPath for jail-aware host path translation#9

Merged
jlrickert merged 1 commit into
mainfrom
feat/host-paths
May 26, 2026
Merged

feat(runtime): add HostPath for jail-aware host path translation#9
jlrickert merged 1 commit into
mainfrom
feat/host-paths

Conversation

@jlrickert

Copy link
Copy Markdown
Owner

Summary

Promotes the previously private OsFS jail-translation helper to a public API on the FileSystem interface and forwards it from Runtime.HostPath. Callers that need a real on-disk path — subprocess argv, file handles handed to external libraries, log output — now have a single stable entry point instead of re-implementing TrimPrefix + filepath.Join(jail, ...) at each call site.

Changes

  • FileSystem.HostPath(virtual) (string, error) added to the interface with godoc covering the canonicalization contract (jail prefix is EvalSymlinks'd before joining; intermediate symlinks in virtual are not canonicalized).
  • (*OsFS).HostPath canonicalizes via EvalSymlinks on the jail root with a raw fallback, so darwin /var/private/var resolves correctly at the API boundary instead of leaking into every caller. Same precedent as the resolveVirtual fix in PR feat(runtime): add Chmod, Chown, Lchown, Chtimes to FileSystem and Runtime #6.
  • Runtime.HostPath forwarder applies the standard validate → ExpandEnvExpandPath → wd-prefix chain before delegating to the FS.
  • toolkit.Edit migrated from inline TrimPrefix + filepath.Join(jail, ...) to the canonical ResolvePath + HostPath two-call form, with godoc noting the rationale. Pinned by the existing TestEdit_UsesRuntimeAndResolvesSymlinkPath regression.
  • ErrSubprocessNotSupported sentinel exported for backends that cannot host a subprocess (TestFS, jailed FSes without a real host mapping). Forward-looking — no consumer in this PR.

Symlink semantics

HostPath is the lexical contract — it canonicalizes only the jail prefix, not the virtual path's intermediate components. Callers that need parent-traversal-symlink defense must resolve through ResolvePath(_, true) first. The darwin canonicalization at the prefix is non-negotiable: without it, a caller that later runs EvalSymlinks on the returned path would see the path move under /private/var/... and fail every subsequent IsInJail check.

TestOsFS_HostPath_LexicalContract_ParentSymlinkNotChased pins this contract explicitly so a future change that conflates HostPath with ResolvePath gets caught.

Tests

9 new tests, all pass with -race:

  • TestOsFS_HostPath_* (5): in-jail virtual path, lexical contract (parent-symlink not chased), no-jail passthrough, relative-uses-wd, darwin /var regression anchor.
  • TestRuntime_HostPath_* (4): forwarder chain coverage including the validate → expand → wd-prefix steps.

Test plan

  • go vet ./... clean
  • gofmt -l toolkit/ clean
  • go test -race -count=1 ./... — all 7 packages pass
  • TestEdit_UsesRuntimeAndResolvesSymlinkPath still green after the Edit migration
  • Darwin /var/private/var canonicalization happens at the API boundary, not in callers

Out of scope

  • Parent-symlink canonicalization in the virtual path. Deferred until a real consumer needs it.
  • ErrSubprocessNotSupported consumer. Sentinel only — wired by a future change.
  • CHANGELOG. Regenerated from conventional commits by the release workflow (.github/workflows/release.yml); the feat: prefix on this commit triggers the next minor bump.

Promote the previously private OsFS jail-translation to a public API on
the FileSystem interface and forward it from Runtime.HostPath, so callers
that need a real on-disk path (subprocess argv, file handles handed to
external libraries, logging) have a single stable entry point instead of
re-implementing TrimPrefix + filepath.Join(jail, ...) at each call site.

- FileSystem.HostPath(virtual) (string, error) added to the interface
- (*OsFS).HostPath canonicalizes via EvalSymlinks on the jail root with
  a raw fallback so darwin /var -> /private/var resolves correctly at
  the API boundary instead of leaking into every caller
- Runtime.HostPath forwarder applies the standard validate -> ExpandEnv
  -> ExpandPath -> wd-prefix chain before delegating to the FS
- toolkit.Edit migrated from inline TrimPrefix + filepath.Join(jail, ...)
  to the canonical ResolvePath + HostPath two-call form, with godoc
  noting the rationale; pinned by the existing
  TestEdit_UsesRuntimeAndResolvesSymlinkPath regression
- ErrSubprocessNotSupported sentinel exported for backends that cannot
  host a subprocess (TestFS, jailed FSes without a real host mapping)
- Tests: 5 TestOsFS_HostPath_* (including a darwin /var regression and
  a lexical-contract case for parent-symlink chasing) plus 4
  TestRuntime_HostPath_* covering the forwarder chain
@jlrickert jlrickert merged commit c1afa8f into main May 26, 2026
1 check passed
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