This document is a decision-complete implementation plan for the current full-codebase audit findings. It is a planning document only. Implementation should proceed in stages and should not change unrelated behavior.
The goal is to polish and harden the public and internal code paths without expanding Scriptorium's scope. Keep the existing package boundaries unless a stage explicitly calls for a helper extraction.
Public source options such as `WithPromptFS(fsys, root)` and `WithSchemaFS(fsys, root)` describe `root` as the source boundary, but prompt `content_file` and schema path resolution can clean `..` paths above that root when the supplied `fs.FS` is broader than the configured root.
For public `fs.FS` source options, the configured root is a containment boundary. Prompt `content_file` paths and schema paths must resolve inside that root. Absolute paths and relative traversal that escape the root are invalid.
1. Add a small shared internal helper for `fs.FS` path resolution.
- Prefer `internal/filecatalog` if the helper naturally belongs with existing clean/display path utilities.
- Inputs should include a source root and a user path.
- It should trim whitespace, clean slash paths with `path.Clean`, reject empty paths where the caller requires a file, reject absolute paths, and reject any path whose clean form escapes the clean root.
- Use path-component checks, not string-prefix checks alone.
- Return both the resolved `fs.FS` path and a display path when useful for errors.
-`WithSchemaFS(fsys, root)` should allow schema paths inside `root`.
-`WithSchemaFile(path)` should keep the existing single-file behavior: prompt `schema_path` must match the selected file's base name.
- Schema paths that escape the configured root should return validation/schema-load errors.
4. Preserve directory-backed compatibility unless a failing test reveals an inconsistency that must be fixed.
-`WithPromptFile(path)` should continue resolving `content_file` values relative to the selected prompt file's directory.
-`Config.SchemaDir` / CLI `--schema-dir` should keep documented behavior, including absolute `schema_path` support, because these are operator-controlled local filesystem paths.
HTTP `serve` decodes request bodies directly from `r.Body`, reads file artifacts fully into memory, and serializes generated artifact bodies fully into the response. This is acceptable for trusted small local use, but it is not hardened against accidental or hostile large inputs.
Add configurable HTTP size limits with conservative defaults. Limits apply only to HTTP `serve`; CLI `run` and `render` keep existing direct filesystem behavior.
Default limits:
-`server.max_request_bytes`: 16 MiB.
-`server.max_artifact_bytes`: 16 MiB.
-`server.max_response_bytes`: 16 MiB.
Use `0` to disable a specific limit only where this is consistent with existing config style. Negative values are invalid config.
- Apply built-in defaults, config-file values, and CLI overrides according to existing precedence.
- Reject negative values.
3. Add `serve` CLI overrides.
-`--max-request-bytes`
-`--max-artifact-bytes`
-`--max-response-bytes`
- Keep these flags scoped to `serve`.
4. Extend HTTP handler construction.
- Add `httpadapter.HandlerOptions` with request and response limit fields.
- Keep `httpadapter.NewHandler(runner)` as a default constructor for existing tests and callers.
- Add `httpadapter.NewHandlerWithOptions(runner, options)` for `serve` wiring.
-`NewHandler(runner)` should apply built-in defaults.
-`NewHandlerWithOptions(runner, options)` should use the supplied values exactly, so `0` means disabled after config validation.
5. Limit request decoding.
- Wrap `r.Body` with `http.MaxBytesReader` when `max_request_bytes > 0`.
- Return `413 request_too_large` when decoding fails due to size.
- Continue returning `400 invalid_json` for malformed JSON.
- Ensure the decoder rejects trailing JSON tokens if it does not already.
6. Limit HTTP file artifact reads.
- Add a max-bytes option to the restricted HTTP artifact reader.
- Use `os.Open`, `Stat`, and `io.LimitReader` or equivalent instead of unbounded `os.ReadFile` for restricted HTTP file reads.
- If a file exceeds the configured limit, return a specific artifact error that maps to `413 artifact_too_large`.
- Keep inline artifact bodies covered by the request-body limit.
7. Limit HTTP response artifact bodies.
- Build the response DTO, marshal it to JSON bytes, and compare the final encoded response size against `max_response_bytes` when the limit is positive.
- Return `413 response_too_large` when the encoded response exceeds the configured limit.
- Do not truncate successful artifacts silently.
- Apply the same encoded-response check when `include_raw_output` is true.
`OpenAICompatibleProfile` currently deep-copies `ExtraParams` through the general internal copy helper. Cyclic caller-provided maps can recurse indefinitely before `WithProfiles` can validate and return `ErrInvalidConfig`.
`OpenAICompatibleProfile` is a convenience constructor, not a validator. It must not recursively walk caller-provided `ExtraParams`. Validation and safe deep copying belong in `WithProfiles` through the existing public JSON validation path.
The OpenAI-compatible client includes a trimmed provider response body snippet in `ErrUnexpectedStatus`. CLI and library callers may log this error. Provider error bodies can include prompt fragments, schema details, request IDs, or other sensitive operational data.
Default errors should include the provider status code but not the response body. Do not add a debug mode in this pass unless an existing debug/logging surface already supports it.
The restricted HTTP artifact reader uses lexical path containment before reading the file. Symlinks inside the artifact root are followed by the operating system. This is documented, but the phrase "must stay inside the root" can be overread as a strict realpath guarantee.
Keep symlink-following behavior for this cleanup pass, but make the code and docs explicit that containment is lexical and relies on the artifact root not being writable by untrusted users. This avoids a potentially breaking change for deployments that intentionally use symlinks.
Implementation:
1. Rename or comment the restricted reader's path-resolution helper to make the lexical nature clear.
2. Add tests documenting current symlink behavior where the platform supports symlinks.
- A symlink inside the root to a file outside the root is followed.
- The test should skip cleanly if symlink creation is unavailable.
3. Keep traversal rejection tests for `..` and absolute paths outside the root.
4. Do not introduce `filepath.EvalSymlinks` in this pass.
Documentation:
- Update `docs/config.md`, `docs/operations.md`, `docs/integrations/http-api.md`, and `docs/internal/adapters.md` to say:
- lexical traversal outside the root is rejected;
- symlinks inside the root are followed;
- the artifact root must not be writable by untrusted users.
- Avoid wording that implies strict realpath containment unless the implementation changes to enforce it.
Future option:
If strict filesystem containment becomes required, add an opt-in or replacement mode that resolves both the configured root and requested file with `filepath.EvalSymlinks` before reading, rejects symlink escapes, and documents any compatibility impact.
## Stage 6: Align CLI Help And Documentation
Problem:
The `serve` usage text omits `--artifact-root`, even though the flag exists. New limit flags from Stage 2 also need to appear consistently in CLI help and docs.