Skip to content

Fix orchestration file generation when /app is root-owned (V2) - #1626

Merged
Paul Lizer (paullizer) merged 6 commits into
paullizer-react-v2-uifrom
paullizer-investigate-orchestration-failures
Oct 3, 2026
Merged

Paul Lizer (paullizer) merged 6 commits into
paullizer-react-v2-uifrom
paullizer-investigate-orchestration-failures

Conversation

@paullizer

@paullizer Paul Lizer (paullizer) commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Problem: On the V2 branch, chat orchestration couldn't create downloadable files. Every CSV, JSON, XML, YAML, TXT, and Markdown file failed with output_access_denied ("This file could not be created."). Downloading any orchestration-generated file, Word and PDF included, used the same failing step.
  • Root cause: Since the OneNote runtime stage (c9faeae), /app in the image is owned by root, while the app runs as UID 65532 with /app as its working directory. The renderer and the verified download stream created scratch files with tempfile.TemporaryFile(dir='.'), so the OS refused them. output_failure() reports any PermissionError as an access refusal (output_access_denied), which hid the real cause. Confirmed from the deployed image layers: before c9faeae, /app was owned by 65532 and a CSV render completed. After it, /app is owned by 0:0.
  • Fix: A new import-light functions_temp_files.scratch_file_dir() returns /sc-temp-files when it's writable, otherwise the platform temp directory, and never the working directory. Both call sites use it. In the Dockerfile, the onenote-runtime stage is removed and the final stage builds from the pinned mcr.microsoft.com/azurelinux/distroless/python:3.12 image again, as it did before c9faeae. The extractor's notices, licenses, and source are copied at the top of the final stage. The extractor binary is copied after the app code, so /app is created by the runtime user's chowned copy. The binary stays root-owned.
  • Diagnostics: For a failed attempt, the [ORCHESTRATION_EXECUTOR] A file render attempt finished. event now logs sc_error_type, sc_error_cause_type, and sc_error_errno: class names and errno only, never messages or paths. The user-facing classification is unchanged.

Review notes:

  • Either change alone fixes the failure. I kept both so a later image-layout change can't silently break file generation again.
  • Two existing tests asserted dir='.' ("must stay in the project"), which pinned this defect. They now assert the scratch_file_dir() location. A sandbox whose system temp directory isn't writable should set TMPDIR/TEMP instead.
  • Malicious PR Security Review: this check inspects the whole Dockerfile whenever a PR touches it, and flagged FROM onenote-runtime because a build-stage name has no tag or digest. Removing the stage clears that false positive without changing the checker. The check passes on this PR, and run locally against the current base it reports no blocker findings.
  • functional_tests/test_v2_onenote_uploads.py previously required the final stage to inherit onenote-runtime. It now requires the distroless base and the extractor binary, notices, licenses, and source in the final stage itself. The image contents and paths are unchanged.
  • Interaction with Saved chat, Analyze and orchestration results take their container's access (screening layer 3b) #1632: Saved chat, Analyze and orchestration results take their container's access (screening layer 3b) #1632 changed output_access_denied to mean access to the file's conversation could not be confirmed, and a changed source no longer fails a retained file. The PermissionError classification is unchanged, so a file-system error still shares that code. The new diagnostics tell the two apart, and this PR's notes call it an access refusal rather than a source-access refusal.
  • I merged the latest paullizer-react-v2-ui and renumbered to 0.261.233, because the target had already used 0.261.218 and 0.261.228 through 0.261.232.

Linked issue

Fixes #1623

Release Notes & Latest Features

  • New Feature
  • Bug Fix
  • UI Enhancement
  • Breaking Change
  • Internal only

Is this visible to end users?

  • Yes
  • No

Is this admin-facing (Admin Settings, governance, deployment, config)?

  • Yes
  • No

Should this become a Latest Feature card?

  • Yes
  • No
  • Already added

Screenshot needed for the card?

  • Yes
  • No
  • Attached

Version bump

  • application/single_app/config.py VERSION third segment bumped, or not needed because this is docs-only (0.261.232 -> 0.261.233)
  • deployers/version.txt bumped, or not needed because deployers/ was not changed

Testing / validation

  • On the merged tree, I ran each file in its own process with $env:PYTHONPATH = "$PWD\application\single_app;$PWD\functional_tests"; python -m pytest -q -p no:cacheprovider <file>. All passed:
    • functional_tests/test_orchestration_file_render_scratch_dir.py (new): 6 passed
    • functional_tests/test_generated_file_structured_renderers.py: 300 passed
    • functional_tests/test_orchestration_output_lifecycle.py: 218 passed
    • functional_tests/test_orchestration_output_cleanup.py: 72 passed
    • functional_tests/test_workflow_saved_output_artifacts.py: 42 passed
    • functional_tests/test_orchestration_output_parent_claim.py: 30 passed
    • functional_tests/test_orchestration_output_history_routes.py: 25 passed
    • functional_tests/test_saved_results_container_access_chat_orchestration.py: 23 passed, 6 subtests passed
    • functional_tests/test_document_provenance_publication.py: 18 passed
    • functional_tests/test_orchestration_render_check_cadence.py: 15 passed
    • functional_tests/test_orchestration_artifact_publication.py: 13 passed
    • functional_tests/test_v2_onenote_uploads.py: 8 passed, 13 subtests passed
  • Regression proof: with only the two scratch-file call sites reverted to dir='.', the new end-to-end test fails with a failed render, as production did, and the static guard reports both lines. Both pass with the fix.
  • Dockerfile structure: both structure tests fail on the base's Dockerfile and on this branch's previous Dockerfile (FROM onenote-runtime), and pass on the new one.
  • python scripts/check_malicious_pr_security_review.py --base-sha <base> --head-sha HEAD against the current base: no blocker findings. Run directly, its Dockerfile rule finds 1 blocker in the base's Dockerfile and 0 in this PR's.
  • Broader sweep, before the target merges: I ran all 70 test files that reference the changed modules, one process per file. Of the 22 files that failed, 21 fail the same way on a clean HEAD checkout. Causes include stale fixtures, a local werkzeug version mismatch, existing docs link and logging-tag findings, and release-notes pages that were already out of date. The remaining file was test_workflow_saved_output_artifacts.py; I fixed it by passing scratch_file_dir into the namespace it loads the function into. Separately, test_tabular_row_orchestration_scale.py timed out after 45 minutes on both HEAD and this branch.
  • I ran the local tests in a venv layered on the global site-packages, with pyOpenSSL upgraded. The global pyOpenSSL 25.3.0 doesn't work with cryptography 50.0.0.
  • git diff --check and python -m py_compile on the changed Python files: clean.
  • Production evidence: App Insights recorded the failed render as output_access_denied after about 261 ms, with 1 full check, 1 light check, and 1 source recheck. I also inspected /app ownership in the deployed image layers in ACR.
  • Not run: a full image build. The Docker daemon isn't available locally. After the next image build, confirm that /app is owned by 65532, that the OneNote notices exist under /usr/share/simplechat/onenote-extractor/, and that an orchestration CSV completes and downloads.

Documentation

  • Release notes updated, or not needed (v0.261.233 Bug Fixes entry)
  • Feature documentation updated, or not needed (ONENOTE_INGESTION.md build layout, docs/reference/logging-tags.md render-attempt fields)
  • Fix documentation updated, or not needed (docs/explanation/fixes/ORCHESTRATION_FILE_RENDER_PERMISSION_FIX.md)

Security checklist

  • New Flask routes include @swagger_route(security=get_auth_security()) (N/A: no routes added or changed)
  • Settings sent to non-admin frontends use sanitize_settings_for_user() (N/A: no settings sent to the frontend)
  • Browser JavaScript is served from local SimpleChat static assets only; no CDN-hosted JS (N/A: no browser assets changed)
  • No secrets, keys, connection strings, or local-only artifacts are included

Paul Lizer (paullizer) and others added 6 commits October 2, 2026 18:45
Since the OneNote runtime stage (c9faeae), /app in the image is owned by
root while the app runs as UID 65532 with /app as its working directory.
The structured file renderer and the generated-file download stream created
scratch files with tempfile.TemporaryFile(dir='.'), so every CSV, JSON, XML,
YAML, TXT and Markdown render failed with PermissionError, reported as
output_access_denied.

- Add functions_temp_files.scratch_file_dir(): /sc-temp-files when it is
  writable, otherwise the platform temp directory, never the working
  directory. Use it in both call sites.
- Dockerfile: onenote-runtime no longer creates /app. Copy the extractor
  binary in the final stage after the app code, so the runtime user's
  chowned copy creates /app. The binary stays root-owned.
- Record error_type, error_cause_type and error_errno in render-attempt
  diagnostics, without messages or paths.
- Add regression tests. Update two tests that pinned dir='.' and one
  source-loaded namespace.
- Docs: fix document, logging tags, OneNote build layout, release notes.
  Bump to 0.261.218.

Fixes #1623

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The target branch reached 0.261.227 and already used 0.261.218 for another
fix. Move this fix's release notes entry to a new 0.261.228 section, keep
the target's 0.261.218 entry, and renumber the fix document, the logging
reference note and the regression test header.

Refs #1623

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The target branch took 0.261.228 for the V2 workflow alert card change
(#1625). Keep its 0.261.228 release notes entry, move this fix to a new
0.261.229 section, and renumber the fix document, the logging reference
note and the regression test header.

Refs #1623

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The target branch moved to 0.261.231 (#1627 through #1631) and now uses
0.261.229, 0.261.230 and 0.261.231. Keep those release notes entries, move
this fix to a new 0.261.232 section, and renumber the fix document, the
logging reference note and the regression test header.

Refs #1623

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The final stage inherited the onenote-runtime build stage. That stage once
created /app as root, and the Malicious PR Security Review flags FROM
onenote-runtime as a floating base image on any PR that touches the
Dockerfile, because a stage name has no tag or digest.

After the previous commit the stage held only the OneNote extractor's
notices, licenses and source. Remove it, build the final stage FROM
mcr.microsoft.com/azurelinux/distroless/python:3.12 as before c9faeae,
and copy those files from onenote-builder at the top of the final stage.
The image contents and paths are unchanged, and the checker is not
modified.

- The scratch-dir regression test now requires that the final stage does
  not inherit any build stage and that its base is pinned.
- test_v2_onenote_uploads requires the distroless base and the extractor
  binary, notices, licenses and source in the final stage itself.
- Update the OneNote build layout and the fix document.

Refs #1623

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The target branch took 0.261.232 for #1632, where saved chat, Analyze and
orchestration results take their container's access. Keep its release notes
entry, move this fix to a new 0.261.233 section, and renumber the fix
document, the logging reference note and the test headers.

#1632 also changed what output_access_denied means: its message now says
access to the file's conversation could not be confirmed, and a changed
source no longer fails a retained file. The PermissionError classification
is unchanged. Reword this fix's notes, which called the code a source-access
refusal, to call it an access refusal.

Refs #1623

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@paullizer
Paul Lizer (paullizer) merged commit f7e268c into paullizer-react-v2-ui Oct 3, 2026
11 checks passed
Paul Lizer (paullizer) added a commit to paullizer/simplechat that referenced this pull request Oct 3, 2026
The base now ships its own v0.261.233 release-notes section (microsoft#1626, microsoft#1633),
so this branch's entry moves to a new v0.261.234 section above it. The base
section is kept verbatim. config.py merged cleanly because both sides set
0.261.233; the renumbering follows in the next commit.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer) added a commit to paullizer/simplechat that referenced this pull request Oct 6, 2026
While this branch was being synced, paullizer-react-v2-ui merged microsoft#1626
and microsoft#1633 and published v0.261.233, which this branch had just taken for
its first merge phase.

The integration branch's sections are kept exactly as published. This
branch's eight sections shift up one, to v0.261.234 through v0.261.241,
and config.py becomes 0.261.241. Every file only this branch changed is
checked to equal its previous content with those versions shifted, and
the release notes are the integration branch's file plus this branch's
eight sections.

functions_structured_file_renderers.py conflicted on imports only: the
exact-tabular profiles import from this branch and the scratch directory
import from the integration branch are both kept.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer) added a commit that referenced this pull request Oct 6, 2026
Brings in #1626 (orchestration file render permission fix, released as
0.261.233) and #1633 (roadmap updates). The only conflict was the top of
release_notes.md, where both sides added a v0.261.233 section; both are
kept intact under their own headings. 6b-2 is renumbered to 0.261.234 in
the next commit.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer) added a commit that referenced this pull request Oct 6, 2026
#1626 released 0.261.233 first, so 6b-2 moves to the next patch version:
config.py, its release notes heading, and every 6b-2 version reference in
its feature docs, test headers and version assertions. #1626's own
0.261.233 references are unchanged.

Refs #1546

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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