fix(mothership): explain withheld code and workflow results and bound workflow block logs - #8459
waleedlatif1 wants to merge 14 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
There was a problem hiding this comment.
All reported issues were addressed across 14 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
aca53b7 to
98b6782
Compare
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 17 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 17 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
No issues found across 17 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.
Re-trigger cubic
…rossing A run_code / run_function that mounted a workspace file whose sidecar is `unknown` latched its per-call registry through the crossing with only `source-provenance-incomplete` and `inherited-incomplete-source`. Both are in the registry's absence set, so the withheld result named no guard, and an output file written from that registry (outputs.files[].path) was recorded as `unrecorded` absence instead of taint. The crossing now inherits the mounted registry's own reasons when it latched, so the refusal names `mounted-file-provenance-unavailable` and writers keep the taint. The mount refusal also reports the file id. No projection is relaxed: the result is still withheld, exact mounts still redact, clean mounts are unchanged.
A withheld result reached the model as a bare `{ success: true }`, so the agent
could not tell a tainted input from an oversized payload and retried or
guessed. The withheld result now carries `withheldReason`, chosen from
code-defined wording by the guard that tripped: an input file, table, or
document with unknown secret provenance; provenance that could not be
verified; or content that could not be checked. It rides the existing
`resultWithheld` disclosure beside any effect ids, and the key is reserved so
an id cannot displace it.
No content, reason literal, or origin crosses; an absent registry still
carries nothing.
A `content-refused` withholding said only that a complete registry refused the payload. It can be refused by its encoded size or by the number of values the projection walks. Row-shaped payloads reach the 100k-value traversal cap well before the 16 MiB byte cap, and only while the call has an active secret. The withheld log lines now report the result's encoded bytes and value count, so a refusal names which cap it hit. Numbers only; no content is logged.
…et projection A run_workflow result echoes every block's output in `logs`. A run with many row-shaped block outputs can exceed the projection's 100k-value traversal cap while staying under its byte cap. Whenever the call had an active secret, the whole result was then withheld, including the final output and error, and the model saw a bare success. Block-log outputs are now bounded to a quarter of each projection cap (`MAX_CONTENT_NODES` values and the default byte cap). Past the budget, the bulkiest outputs are replaced, largest first, with a `logs get <executionId> --trace` pointer, in the same form the oversized-input compaction already uses. The executionId, status, final output, error, and `select` values are untouched, and the other three quarters of each cap stay free for them.
… length-blind run_workflow can complete in the browser, and that restoration projected the raw block logs, so a large browser-run workflow was still withheld. The block-log compaction now lives in the shared workflow-output module and applies to both the server handler and the client restoration when no `select` is given. `select` still reads full values. The omitted-output pointer no longer reports bytes. They were measured before secret projection and so disclosed a secret's length. It reports the value count and says "inspect with logs get" rather than promising the full value. One `measureModelContent` in the projection module now serves both the compaction and the withheld-size logging. It stops counting at the value cap and tolerates values JSON cannot encode. The byte cap is exported beside `MAX_CONTENT_NODES`.
…jection The browser-run restoration projected the full raw block logs and only then applied `select`, so a large run was still withheld whenever a selector was given. The server handler selects first. Both paths now build their log fields through one shared `presentWorkflowLogsForModel`, from raw logs and before projection: a `select` resolves against the full logs and replaces them; otherwise the echoed logs are bounded. Selected values are still projected, so a selected secret is redacted.
The provider tool path projects through the same withheld-result shape, so an omitted model result now carries `resultWithheld` and its fixed-wording `withheldReason`. The expectations still asserted the old empty output.
…d a walk-based measure - run_block and run_workflow_until_block lift the stopping block's output into `output`. That copy is now bounded with the same budget and pointer as a block-log output, so one huge block no longer gets the whole response withheld. - The truncated-input marker no longer carries the input's length. It is written before secret projection, so the length disclosed a secret's length. - `measureModelContent` now walks the value by JSON's rules instead of serializing it. It stops at the first value, byte, or depth limit it passes and reports `exceeded`, measuring a string only while it fits the remaining byte budget. An output past a limit, including one nested past the projection's depth limit, becomes a pointer instead of voiding the run. - The browser-run restoration now truncates echoed block inputs as the server handler does: both paths build their log fields through `presentWorkflowLogsForModel`.
A truncated block input kept its first 200 raw characters, written before secret projection. A secret straddling that cut left a fragment that whole-literal redaction cannot match, so part of the secret reached the model. The marker now keeps nothing of the raw input: `…[input omitted; inspect with logs get <executionId> --trace]`. Inputs over the limit are echoed upstream data the caller already has or can fetch, so the preview carried little.
…m against a secret Without an active secret the model-facing projection passes JSON through under its byte cap alone, so the block-output budget turned a large lifted run_block output into a pointer for no reason. Output compaction now applies only when the call's registry makes the projection walk the result, and a lifted output keeps the final output's share beside the bounded logs.
…its final output A fixed share for the lifted output left no room for the log entries, envelope and error the projection also counts, and a Response block's final output was never bounded, so a run near the caps was still withheld whole. The assembled result is now measured as the projection will walk it, and a final output that would push it past a cap is replaced with a pointer, on the server and browser-run paths alike. An unencodable result is still refused as before.
… to refuse The output bound handles size only. A block output JSON cannot encode cannot be checked, so it is no longer sized past the budget and replaced with a pointer; the projection refuses it as it did before this PR.
…without a secret, as staging returns it Block-log outputs were bounded whenever a secret was active, so a 30k-value or 4.5 MB result that crossed in full before became pointers. Outputs are now bounded only when the whole result would pass a projection cap. Without an active secret the server handler keeps its preview input marker and the browser-run path leaves its logs untouched, in their original position, so a result with no secret is byte-identical to before; only a walked call gets the marker that keeps nothing.
0ef9fe6 to
2c0b369
Compare
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
No issues found across 17 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.
Re-trigger cubic
…he whole-result measure The unencodable-output test placed the BigInt in the lifted output, which the whole-result walk reaches first, so it passed without the guard. It now puts a bulky log ahead of the BigInt log. A new test fails if the error is left out of the measure the result is bounded against.
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
No issues found across 17 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.
Re-trigger cubic
Summary
Successful
run_code,run_functionandrun_workflowcalls could reach the model as a bare{ success: true }with no output, and the model never learned why. This PR fixes what can be fixed without weakening the secret boundary: a resolved secret still never reaches the model.Keep a tainted mount's verdict across the run_code crossing.
unknown, its result is still withheld. That is correct, because the code can read those bytes.source-provenance-incomplete, a reason in the registry's absence set. The refusal named no guard, and a file written from that registry (outputs.files[].path) was recorded asunrecordedabsence instead of taint.mounted-file-provenance-unavailable). Mount refusals also report the file id.Tell the model why a result was withheld.
withheldReasoninside the existingresultWithhelddisclosure. The text is fixed wording chosen by the guard that tripped:Log the size of a withheld result.
resultBytesandresultValues(numbers only), so acontent-refusedrefusal can be traced to the byte cap or to the traversal cap.measureModelContentdoes the counting. It walks the value by JSON's rules instead of serializing it, and stops at the first value, byte or depth limit, so a huge payload is never serialized just to be measured.Bound
run_workflowblock-log outputs, on both the server and browser paths.run_workflowresult echoes every block's output. When a call has an active secret, the projection refuses any payload whose walk exceeds its 100k-value traversal cap. So a synthetic run whose block outputs exceed that cap, while staying under the byte cap, was withheld whole.copilotProjectionWalksContent) mirrors the projection's own matcher on the same per-call registry.MAX_CONTENT_NODESand the exported default byte cap), and if it still would, its final output too. This applies to the server handler and to the restoration of a browser-run workflow.…[output omitted: N values; inspect with logs get <executionId> --trace].outputbyrun_blockorrun_workflow_until_block, on both paths. A result or block output JSON cannot encode (a cycle, a BigInt) is never replaced: it cannot be checked, so the projection refuses it as before. The bound handles size only. A block-log output past the projection's depth limit becomes a pointer instead of voiding the run.selectvalues are untouched and keep the remaining headroom.selectresolves against the full logs and replaces them, so a browser-run workflow with a selector is no longer withheld either. Selected values are still projected.Testing
run_code: runs through the real handler, projection and file-classification path. Before the fix, the refusal named no guard and the output file was classified asunrecorded. Exact-secret mounts still redact and clean mounts stay readable.run_workflowbudget tests (synthetic trace-shaped runs):selectreturns full values and bypasses the budget. Compacting regardless ofselectturns this test red.selectprojects the selected values, with the secret redacted, instead of being withheld. This test failed before the fix.measureModelContentreports strings past the byte cap, content past the value cap, and content nested past the depth limit as over, and refuses BigInt and cycles.bun run lint,bun run type-checkandbun run check:auditspass.lib/mothership,lib/execution,executor/utils,providersand Copilot/function route suites pass (6,177 passed, 23 skipped).apps/simVitest on this change: 34,747 passed, 25 skipped, 0 failed.