Skip to content

exec.writeFile/addFile: { writable: true } opts knob#1648

Open
dbolotin wants to merge 8 commits into
mainfrom
feat/exec-writable-files
Open

exec.writeFile/addFile: { writable: true } opts knob#1648
dbolotin wants to merge 8 commits into
mainfrom
feat/exec-writable-files

Conversation

@dbolotin
Copy link
Copy Markdown
Member

@dbolotin dbolotin commented May 20, 2026

Summary

Opt-in escape hatch for the 0o400 default workdir fill-rule perms set in #1628. Passing { writable: true } to exec.builder().addFile, .writeFile (and the plural forms addFiles / writeFiles) lands the file as 0o600 in the workdir, forcing the backend to copy from the archive/value cache to a fresh inode instead of taking the hardlink fast path.

The same option is mirrored on workdir.builder().addFile / writeFile (plus plural forms) — that's the layer that actually emits the fill-rule permissions field. exec-impl.tpl threads a writableFiles name set through settings and applies it per file.

b.addFile("input.tsv", fileRef)                              // RO 0o400 — hardlink
b.addFile("input.tsv", fileRef, { writable: true })          // RW 0o600 — copy
b.writeFile("config.json", { foo: 1 })                       // RO 0o400 — hardlink
b.writeFile("config.json", { foo: 1 }, { writable: true })   // RW 0o600 — copy
b.addFiles({ a: refA, b: refB }, { writable: true })         // all RW
b.writeFiles({ a: "x", b: "y" }, { writable: true })         // all RW

Compatibility

When no caller marks any file writable, the new writableFiles field is omitted from the settings JSON resource, so existing workflows produce the exact same exec CID. Old callers also keep working — the schema additions are all ,? (optional).

Background

The default landed in #1628 to make the backend's addFileToWorkdir take the hardlink path: it compares the fill-rule mode against mifs.ArchiveEntryPerms (0o400) and only hardlinks on exact match. Anything else forces a copy. The changeset there promised an explicit override mechanism — this PR is that knob, surfaced at the public API.

Greptile Summary

This PR adds a { writable: true } opt-in escape hatch to exec.builder() and workdir.builder() file-addition methods so callers can land files as 0o600 (copy-on-use) instead of the default 0o400 (hardlinked from the archive cache). When no file is marked writable the new writableFiles field is omitted from the settings JSON via maps.clone({removeUndefs: true}), keeping the exec CID identical for all existing workflows.

  • exec/index.lib.tengo: tracks a writableFiles set in addFile / addFiles / writeFile; serialises it only when non-empty and forwards it to the impl template.
  • exec-impl.tpl.tengo: reconstructs the set at runtime and calls wdBuilder.addFile/writeFile with the per-file writable flag.
  • workdir/index.lib.tengo: all four methods (addFile, addFiles, writeFile, writeFiles) updated; _getFileFillRule and _getValueFillRule now accept a writable parameter and emit 0o400 or 0o600 accordingly.

Confidence Score: 4/5

The core implementation is sound; the only gap is a changeset doc that promises a method not present in the code.

The writableFiles tracking, serialisation, and fill-rule emission are consistent across both builder layers. The changeset text promises exec.builder().writeFiles() accepts { writable }, but that method was never added to exec/index.lib.tengo — a user following the docs would hit a runtime error. CID stability via removeUndefs, the undefined guard in the impl template, and sets.add mutation semantics are all correct.

sdk/workflow-tengo/src/exec/index.lib.tengo and .changeset/exec-writable-files.md — either add a writeFiles method to the exec builder or correct the changeset description.

Important Files Changed

Filename Overview
.changeset/exec-writable-files.md Changeset doc incorrectly implies exec.builder() has a writeFiles plural method; only workdir.builder() has it.
sdk/workflow-tengo/src/exec/exec-impl.tpl.tengo Builds a writableSet from settings.writableFiles and threads it into wdBuilder.addFile/wdBuilder.writeFile per-file — logic is correct, undefined-guard is sound.
sdk/workflow-tengo/src/exec/exec.tpl.tengo Adds writableFiles to schema validation and forwards it through maps.clone({removeUndefs:true}) to settings — CID stability for non-writable workflows preserved.
sdk/workflow-tengo/src/exec/index.lib.tengo Adds writableFiles set tracking in addFile and writeFile; addFiles properly tunnels opts; slices.fromSet serialization only when non-empty. Missing a writeFiles plural method that the changeset doc promises.
sdk/workflow-tengo/src/workdir/index.lib.tengo All four methods (addFile, addFiles, writeFile, writeFiles) updated consistently; _getFileFillRule and _getValueFillRule accept a writable bool and emit 0o400/0o600 accordingly.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant exec.builder
    participant exec.tpl
    participant exec-impl.tpl
    participant workdir.builder

    Caller->>exec.builder: "addFile(name, ref, {writable: true})"
    exec.builder->>exec.builder: sets.add(writableFiles, name)
    Caller->>exec.builder: run()
    exec.builder->>exec.tpl: "pureExecInputs.writableFiles = slices.fromSet(writableFiles)"
    exec.tpl->>exec-impl.tpl: "settings = {writableFiles: [...], ...}"
    exec-impl.tpl->>exec-impl.tpl: build writableSet from settings.writableFiles
    exec-impl.tpl->>workdir.builder: "addFile(name, ref, {writable: writableSet[name]==true})"
    workdir.builder->>workdir.builder: sets.add(writableFiles, name) if writable
    exec-impl.tpl->>workdir.builder: "writeFile(name, ref, {writable: writableSet[name]==true})"
    workdir.builder->>workdir.builder: "build() emits fill rule with permissions=0o600 or 0o400"
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
.changeset/exec-writable-files.md:4
**Changeset claims `exec.builder().writeFiles` accepts `{ writable }`** — this method doesn't exist

The opening sentence reads *"(and the plural `addFiles` / `writeFiles`)"* as if both plural forms live on `exec.builder()`. `addFiles` is there, but `exec.builder()` has never had a `writeFiles` method (only the `workdir.builder()` does). A developer reading this changeset and calling `exec.builder().writeFiles({...}, { writable: true })` would get a runtime error. Either add `writeFiles` to the exec builder or tighten the sentence to distinguish which plurals belong to which builder.

Reviews (1): Last reviewed commit: "exec.writeFile/addFile: { writable: true..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Adds an opt-in escape hatch for the 0o400 default fill-rule perms set in
PR #1628. Passing { writable: true } to exec.builder().addFile,
.writeFile (and the plural forms) lands the file as 0o600 in the
workdir, forcing the backend to copy from the archive/value cache to a
fresh inode instead of taking the hardlink fast path.

Same option is mirrored on workdir.builder().addFile / writeFile (plus
plural forms) — that's the layer that actually emits the fill-rule
permissions field. exec-impl.tpl threads a writableFiles name set
through settings and applies it per file.

When no caller marks any file writable, the new settings field is
omitted, so existing workflows keep the exact same exec CID.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 20, 2026

🦋 Changeset detected

Latest commit: 8573c76

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
Name Type
@platforma-sdk/workflow-tengo Minor
@milaboratories/pl-client Minor
@milaboratories/pl-middle-layer Patch
@milaboratories/milaboratories.monetization-test.workflow Patch
@milaboratories/milaboratories.ui-examples.workflow Patch
@milaboratories/milaboratories.pool-explorer.workflow Patch
@milaboratories/pl-model-backend Patch
@milaboratories/pl-errors Patch
@milaboratories/pl-tree Patch
@milaboratories/pl-drivers Patch
@platforma-sdk/pl-cli Patch
@platforma-sdk/test Patch
@milaboratories/milaboratories.monetization-test Patch
@milaboratories/milaboratories.ui-examples Patch
@milaboratories/milaboratories.pool-explorer Patch
@platforma-sdk/tengo-builder Patch
@platforma-sdk/block-tools Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a { writable: true } option for file operations within exec.builder and workdir.builder, allowing files to be landed with 0o600 permissions instead of the default 0o400. This is implemented by updating the underlying fill rules and propagating the option through the Tengo SDK. The reviewer feedback highlights several opportunities to improve the robustness of the code by adding type checks for optional arguments to prevent potential runtime errors. Additionally, it is recommended to add a writeFiles method to exec.builder to ensure API consistency with workdir.builder.

* @param opts: { writable?: bool, mnz?: ... } - optional flags.
* `writable: true` lands the file as 0o600 in the workdir.
*/
writeFile: func(fileName, data, ...opts) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The exec.builder is missing the writeFiles method, although it is explicitly mentioned in the pull request description and its counterpart addFiles is present. For consistency with workdir.builder and to support the plural form as described, please consider adding it. Since writeFile already handles canonical encoding for maps and arrays, writeFiles can be implemented by iterating over the input map and calling writeFile for each entry.


fileName = path.canonize(fileName)
filesToAdd[fileName] = file
if len(opts) > 0 && !is_undefined(opts[0]) && opts[0].writable == true {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Accessing opts[0].writable without verifying that opts[0] is a map can lead to a runtime error if an unexpected type (e.g., a string or boolean) is passed as an optional argument. While the JSDoc specifies a map, adding a check like ll.isMap(opts[0]) would make the implementation more robust.

if len(opts) > 0 && ll.isMap(opts[0]) && opts[0].writable == true {

fileName = path.canonize(fileName)

filesToWrite[fileName] = data
if len(opts) > 0 && !is_undefined(opts[0]) && opts[0].writable == true {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Accessing opts[0].writable without verifying that opts[0] is a map can lead to a runtime error if an unexpected type is passed. Adding a check like ll.isMap(opts[0]) ensures the builder remains stable even with incorrect usage.

if len(opts) > 0 && ll.isMap(opts[0]) && opts[0].writable == true {

files[fileName] = fileResource
blobs[fileName] = fileResource
self._addDirs(fileName)
if len(opts) > 0 && !is_undefined(opts[0]) && opts[0].writable == true {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It is safer to verify that opts[0] is a map before attempting to access the writable property, preventing potential runtime errors if an invalid type is passed.

if len(opts) > 0 && ll.isMap(opts[0]) && opts[0].writable == true {

validation.assertType(fileName, "string")
values[fileName] = contentRef
self._addDirs(fileName)
if len(opts) > 0 && !is_undefined(opts[0]) && opts[0].writable == true {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Adding a map type check for opts[0] before property access is recommended to ensure robustness against unexpected input types.

if len(opts) > 0 && ll.isMap(opts[0]) && opts[0].writable == true {

Comment thread .changeset/exec-writable-files.md
@codecov
Copy link
Copy Markdown

codecov Bot commented May 20, 2026

Codecov Report

❌ Patch coverage is 0% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.04%. Comparing base (7fd59b5) to head (8573c76).
⚠️ Report is 11 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
lib/node/pl-client/src/core/ll_client.ts 0.00% 8 Missing ⚠️
lib/node/pl-client/src/core/client.ts 0.00% 2 Missing ⚠️

❌ Your patch check has failed because the patch coverage (0.00%) is below the target coverage (50.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1648      +/-   ##
==========================================
+ Coverage   53.02%   53.04%   +0.02%     
==========================================
  Files         270      270              
  Lines       15702    15730      +28     
  Branches     3391     3408      +17     
==========================================
+ Hits         8326     8344      +18     
- Misses       6260     6270      +10     
  Partials     1116     1116              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

dbolotin and others added 7 commits May 20, 2026 19:11
Address PR review:
- Add exec.builder.writeFiles (mirrors addFiles) — the changeset and
  PR description advertised it, but only workdir.builder had it.
- Replace `!is_undefined(opts[0])` with `ll.isMap(opts[0])` at all four
  opts-parsing sites (exec addFile/writeFile, workdir addFile/writeFile)
  so passing a non-map opts argument fails the guard cleanly instead of
  panicking on field access.
Tests
-----
tests/workflow-tengo/src/exec/writable/pt_overwrite.tpl.tengo +
writable.test.ts: parametrized over (mode: write|add, writable: bool).
Drives ptabler with a workflow that reads data.tsv and writes back to
the same path. RW case must succeed (file lands 0o600 → backend copies
to fresh inode); RO case must fail with non-zero exit (file lands 0o400
hardlinked from archive cache → polars hits EACCES). Content roundtrip
is intentionally not asserted because pt's write_csv truncates the
target before reading the source fully; we only assert success vs error.

Version gate
------------
The backend half of this feature lives in milaboratory/pl PR #1830,
merged AFTER the v3.5.0 tag was cut. v3.5.0 release silently ignores
the requested perm and always lands files at the canonical archive
mode (0o400), so the test would spuriously fail.

Added `supportsWritableWorkdirFiles` on `LLPlClient` (and re-exposed on
`PlClient`) — returns true iff `serverInfo.coreVersion` is strictly
after `3.5.0`. Dev builds past the tag like `3.5.0-224-g0ca182` count
as after; the bare `3.5.0` release does not. Implemented via a new
`isAfterVersion` helper alongside the existing `isVersionAtLeast`,
which has different semantics for the exact-triplet case (we need the
tagged release excluded; `isVersionAtLeast` includes it).

The four test cases self-skip on backends that lack the capability,
with a message identifying the backend version.

Verified locally: source build (3.5.0-224-g0ca182) runs all 4 → pass.
Prebuilt 3.5.0 runs all 4 → skip with descriptive reason.
Temporary: tests require pl container running as non-root user so
OS file-mode enforcement is active (writable: false tests). Non-root
behavior lives on github-ci v4-beta until promoted.

Revert to @v4 once v4-beta is merged into v4.
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