Repository navigation
Phase 3b: workflow assist endpoint (POST /api/user/workflows/assist) - #1577
Merged
Paul Lizer (paullizer) merged 12 commits intoSep 30, 2026
Conversation
POST /api/user/workflows/assist turns one instruction into a validated, un-normalized candidate workflow draft for the V2 editor's applyAssist. It never writes a workflow, run or document; its only write is the caller's rate-limit document. - functions_workflow_assist_editor: Python port of the editor's projection, violation and change helpers - functions_workflow_assist_operations: handles, the operation allowlist and schemas, and the structural applier - functions_workflow_assist: request checks, the model envelope, the apply-then-validate loop with one correction round, and warnings - functions_workflow_assist_limits: a per-user Cosmos limiter (one in flight, 20 per 10 minutes) with etag compare-and-swap - functions_workflow_assist_runtime: the model invoker and the Azure-backed services - enable_workflow_ai_assistant admin setting (default on), gated on allow_user_workflows Refs microsoft#1548 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Covers request validation, authorization, the operation allowlist and escalation refusals, apply-then-validate with one correction round, the Phase 2 dry-run parity, the three placement cases, the limiter, the Azure-backed services, content-free telemetry, the no-writes contract and the route's gates, body bounds and status mapping. Sets VERSION to 0.261.205 (provisional). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The field parity test reads the V2 editor's TypeScript lists (authored, forbidden, alert, schedule, runner, flow) and fails when the server's Python copies drift. The candidate parity test runs 24 real assistant requests with a scripted model and replays every result through the editor under node: the opened draft, workflowAssistViolation, the change list and Jump to targets, the Run as consequence, workflowForSave and applyAssist all agree with the server, and refused candidates are refused by both with the same message. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keeps VERSION 0.261.205 over V2's 0.261.204. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds the feature doc, the Workflow admin settings row, an admin section and troubleshooting rows, the features.yml claim and its generated stub, the regenerated app surface inventory, and the v0.261.205 release notes. Refs microsoft#1548 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Read the request body and the model's reply as the browser's JSON.parse does: NaN, Infinity, 1e999 and integers JavaScript reads as Infinity are refused (parse_constant, parse_float and parse_int hooks). - Write assist responses with json.dumps(allow_nan=False); a result that cannot be written is a content-free 500 assistant_failed. - Match JavaScript Number(): ASCII digits only, doubles for radix and integer input, and whole numbers below 1e21 reach the dry run as ints, as they do through the editor's JSON save. - Add a type-strict numeric, whitespace and JSON-literal corpus replayed through the real V2 editor code under Node, compared with json.dumps(sort_keys=True). - Test is_workflow_assistant_enabled_for_user and the V2 bootstrap flag, non-finite, lone-surrogate and over-deep model replies, and a whole expected_count checked as an int. - finished_log raises instead of using a bare assert, so -O keeps it. - Move VERSION to 0.261.206 and document the changes and the flowUnsupportedReason follow-up. Refs microsoft#1548 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Brings in microsoft#1578, which took 0.261.205. Keeps VERSION 0.261.206 and puts the v0.261.206 release-notes section above microsoft#1578's v0.261.205 section, which is unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Drop the unused ASSIST_REPLY_MAX_LENGTH import from functions_workflow_assist.py and the unused WORKFLOW_OUTPUT_KINDS import from functions_workflow_assist_operations.py. - End _checked_focus with an explicit raise, so it can't fall through to None. - Say why each best-effort telemetry except swallows its error. - Restore the "nothing written" assert after the first run of the dry-run correction test. Its writes snapshot was taken but never checked. No behavior change. Refs microsoft#1548 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer)
marked this pull request as ready for review
September 30, 2026 02:08
Paul Lizer (paullizer)
added a commit
that referenced
this pull request
Sep 30, 2026
Versions now follow the order PRs go ready: #1578 takes 0.261.205 and 3b (#1577) takes 0.261.206, so the coordinator moved Phase 4 to 0.261.207. Move VERSION and the headers, docs, version floors and the stubbed bootstrap version this branch set to 0.261.206. Only files this branch changed since V2 2f8dab4 are touched. Refs #1547 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Phase 4 (microsoft#1580) landed first and took 0.261.207, and microsoft#1578 took 0.261.205, so 0.261.206 is skipped. This is a byte-level replace of 0.261.206 with 0.261.208 in every file of this PR, with CRLF preserved; nothing else changes. Refs microsoft#1548 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Merges paullizer-react-v2-ui at 6cc22fc (microsoft#1580) with rerere off. - config.py: VERSION 0.261.208. - release_notes.md: the v0.261.208 section sits above Phase 4's v0.261.207 section, which is unchanged. - test_route_blueprint_policy_inventory.py: keeps Phase 4's proposal coverage line and test, adds the assistant coverage line, and moves Version to 0.261.208. - docs/_data/app_surface.yml: regenerated with scripts/build_docs_inventory.py (capabilities 122). Refs microsoft#1548 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Phase 4 (microsoft#1547, microsoft#1580) made personal File Sync editable: workflowForSave now spreads workflowFileSyncForSave for every scope, so an edited file_sync is sent as workflowFileSyncConfig normalizes it, with sources reduced to scope_type, scope_id and source_id and dropped while File Sync is off (unless the trigger is file_sync). The Python port gains workflow_file_sync_config and workflow_file_sync_for_save and applies them before the alert fields, so the server's projection saves exactly what the editor would. The candidate parity suite gains eleven File Sync save cases checked against the real TypeScript under Node and against pinned answers, and the field parity suite now pins the two File Sync mode lists. The assistant still has no File Sync operations; that remains a follow-up. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Phase 4 has landed, so the doc no longer waits on it: the Python workflowForSave port sends an edited personal File Sync configuration as the editor does, the candidate parity suite covers those saves, and adding File Sync operations to the assistant stays a follow-up. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer)
merged commit Sep 30, 2026
5edb0da
into
microsoft:paullizer-react-v2-ui
10 of 11 checks passed
Paul Lizer (paullizer)
added a commit
to paullizer/simplechat
that referenced
this pull request
Sep 30, 2026
Record 3b (microsoft#1577, v0.261.208) and Phase 4 (microsoft#1580, v0.261.207) as shipped and mark 3c (the Ask AI tab) in progress. Add what later phases build on, their follow-ups, the side fixes from microsoft#1576, microsoft#1578 and microsoft#1579, and gotcha 61 (Microsoft 365 pending-action run links). Docs only; no version bump. Refs microsoft#1543 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This was referenced Sep 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 3b of the AI workflow assistant: the server endpoint that turns one plain-language instruction into a validated candidate draft. It's server only. There's no V2 UI, no stored conversation and no group workflows. 3c's Ask AI tab will call it and hand
candidateto 3a'sWorkflowAuthoringSession.applyAssist.Route.
POST /api/user/workflows/assist, with decorators in this order:@bp.route→@swagger_route(security=get_auth_security())→@login_required→@user_required→@enabled_required('allow_user_workflows')→@workflow_user_required→@workflow_assistant_required.changed,explainedorquestion. The response carries:target.focus_keyfor Jump toCache-Control: no-store, private.It never writes a workflow, run or document. Its only write is the caller's per-user rate-limit document in the settings container. The no-writes test runs a whole request against containers that fail on any write except that document.
Setting.
enable_workflow_ai_assistant, default on.allow_user_workflowsis on and the caller passes theWorkflowUserrole rule.is_workflow_assistant_enabled_for_userbuilds onis_user_workflows_enabled_for_user.features.enable_workflow_ai_assistant.Request (roadmap §5, "Contract").
submission_idbase: {workflow_id, definition_revision} | nullinstruction: 1–2,000 charactersconversation: at most 20 completed turns of up to 4,000 characters eachfocusdraft: the editor'sWorkflowDefinitionreferences: A2's#shapetime_zone: checked againstworkflow_schedule_timezones()request_too_largeand is refused before it's readJSON.parsereads it, with every number finite:NaN,Infinity,-Infinity,1e999and integers JavaScript can only read asInfinityare 400invalid_request, and so is a lone surrogateAuthorization.
group_idreturns 400group_workflow_unsupported.basemust be the caller's own workflow, or it's a 404. A staledefinition_revisionreturns 409workflow_definition_conflict, and the client keeps its draft.#documents go throughresolve_scope_references. Tags return 400tags_unsupported.get_workflow_editor_options(user_id, settings), the same source as the editor-options route. An Azure error there is a 503.No raw IDs reach the model (gotcha 28).
task_N,node_N,agent_N,model_N,alert_N,doc_Nandref_N. Shared references are named by alias, andfocusis sent as a handle.idand alias derived from its file name, unique and valid under_name().Untrusted material (gotchas 5 and 27).
Operations (a closed allowlist, each op validated against its own JSON schema):
reference_ids, or one task'sdocument_actiontarget withtarget_mode: selectedAn ambiguous placement is answered with a Question that changes nothing.
Validation pipeline.
workflowAssistViolation: authored fields only, no forbidden field, no approval change, and no ID that changes meaning.workflowForSave. Whole numbers below 1e21 reach the check asint, as they reach the save route through the editor's JSON.dry_run_personal_workflow.assistant_output_invalid, and nothing changes.Responses are written with
json.dumps(allow_nan=False), because the browser'sresponse.json()can't readNaN. A result that can't be written returns 500assistant_failedand echoes nothing.Warnings.
run_as_reapproval: the Run-as fingerprint changed whilem365_run_as_user_idis setemail_requires_m365_agentm365_not_connected: read from the stored record onlydraft_has_errorsLimits.
check_inbound_mcp_tool_rate_limit: a fixed window under etag compare-and-swap, with a 180 s lease.assistant_limit_unavailable.Retry-After.Model.
_resolve_agent_instruction_modeland_create_agent_instruction_client).max_completion_tokens16,000 foro1,o3andgpt-5(the same family markers as_build_agent_instruction_api_params); otherwisemax_tokens4,000 at temperature 0.2.outcome, a plain-textreplyand typedoperations.Statuses. 200, 400, 403, 404, 409, 413, 429, 502, 503 and 500. Every error has a closed
codeand a safe message, and never echoes model output or document text.Telemetry. One content-free
log_eventper request. It records:user_id,submission_id, status, outcome, code and the failing stageIt never records instructions, drafts, replies or document text.
New modules (all in
application/single_app/):functions_workflow_assist.py: the request, envelope, evaluator, warnings and runfunctions_workflow_assist_operations.py: the handles, the op schemas and the allowlistfunctions_workflow_assist_editor.py: the Python mirror of 3a's lists,workflowAssistViolationandworkflowForSavefunctions_workflow_assist_limits.py: the Cosmos limiterfunctions_workflow_assist_runtime.py: the Azure-backed servicesShared files touched (additive only).
route_backend_workflows.py(the new route and helpers),functions_settings.py,admin_settings_fields.py,route_frontend_admin_settings.py,templates/admin/_panes/workflow.html,route_backend_v2.py, the route inventory andtest_v2_admin_workflow_parity.py's expected dependencies.Docs.
docs/explanation/features/WORKFLOW_AI_ASSISTANT.md, covering the contract, operations, limits and security model. 3c will extend it.docs/admin/workflow.md: a settings row, an "AI workflow assistant" section and troubleshooting rows.features.ymlentry with its generated stub.app_surface.yml.VERSION is 0.261.208. #1578 took 0.261.205, and Phase 4 (#1580) took 0.261.207.
Decisions made autonomously
Setting.
enable_workflow_ai_assistant, default on. It depends onallow_user_workflowsand follows the WorkflowUser role rule.baseusesdefinition_revision, not the roadmap'smodified_at. It's the save route's conflict token, and Phase 2's dry run turns a stale value intoworkflow_definition_conflict→ 409. A deleted base returns 409workflow_deleted.#tags are refused with 400tags_unsupported.reference_inputshold documents only.#never narrows a search here (gotcha 60). A#document is a bound workflow input by construction.No File Sync operations. Trigger operations on a File Sync workflow are refused and explained. Phase 4 (Phase 4: orchestration proposes workflows #1580) made personal File Sync editable, so the Python
workflowForSaveport now mirrorsworkflowFileSyncForSave: File Sync settings the author edited reach the dry run as the editor would save them. File Sync operations are a follow-up.v3 flow drafts. The assistant may change:
It can't add, remove or reorder tasks or set their inputs, because those change the flow graph. The
flowobject must come back unchanged, which is stricter than the editor. For each and If edits are refused and explained.Projection parity. The Python port mirrors
workflowForSave, rather than relying on the dry run accepting the raw draft. Three tests guard it:json.dumps(sort_keys=True)so12and12.0differPersonal workflows off.
allow_user_workflowsoff keeps the shared decorator's existing 400. Changing it to 403 would affect every route. The assistant setting off and the missing role return 403.The limiter fails closed.
Provider throttling returns 503 with
Retry-After, not 429. The provider's value is clamped to 1–60 s, with 10 s when it sends none.No time left for a model call returns 503
assistant_timeout.Advisory lookups (the agent's email capability and the M365 connection) are skipped when less than 5 s remain.
A
changedoutcome with no effective diff is returned asexplained.A draft that already fails a check before the assistant touches it is tolerated. The response carries a
draft_has_errorswarning, and only new errors block.Build failures without a structured message reach the model as a generic "fails a save check" line.
A draft over the prompt budget (360,000 characters) returns 400
assistant_input_too_largerather than being elided.The limits are fixed constants, not admin settings.
Reference IDs. A new reference's ID is its alias.
Task reference lists. When a new reference is bound to selected tasks:
Removing a task drops later tasks' input bindings to it.
Content safety. None beyond the model deployment's own, consistent with the other assists.
Merged V2 three times, never rebased. Every merge used
-c rerere.enabled=false, and I resolved each by hand:2f8dab475before opening this PR, when Remove redundant orchestration progress card from V2 chat #1575 took 0.261.20418dac67d5in review round 2, when Keep workflow Flow canvas nodes stable across renders (#1573) #1578 took 0.261.2056cc22fc8dafter Phase 4 (Phase 4: orchestration proposes workflows #1580) took 0.261.207, which moved this PR from 0.261.206 to 0.261.208The v0.261.208 release-notes section sits above Phase 4's v0.261.207, which is unchanged. Against V2, only my VERSION line and my own release-notes section differ in
config.pyandrelease_notes.md.Non-string enum values are refused, not coerced. The editor keeps a non-string
trigger_type, referencescope_typeor runnertypewhoseString()is allowed (['interval']reads as'interval'). The request refuses a draft carrying one with 400draft_invalidbefore projection, so the server and the editor can't disagree.Numbers follow JavaScript.
Number()is ported with ASCII digits only and JavaScript'strim()set (which includes\ufeffand excludes\x1cand\x85), and every number is rounded as a double. Whole numbers below 1e21 becomeintand-0becomes0, as they do throughJSON.stringifyand the save route. The only numeric operation fields (the interval value andday_of_month) are range-checked, so an exact large integer can't reach a workflow.Non-finite numbers are refused at both inputs. The body and the model's reply are parsed with
parse_constant,parse_floatandparse_inthooks that refuseNaN,Infinity,1e999and integers too large for a double. In the reply, that is a correction round, then 502.Review round 2 (at c3bcd7e)
_JS_DECIMALuses[0-9]._js_doublerounds integer and radix input as JavaScript does.json_numberandstrip_undefinedturn whole floats below 1e21 intointand-0.0into0.test_workflow_assist_candidate_parity.pyadds a type-strict corpus run through the realNumber(),normalizeWorkflowDefinitionandworkflowForSaveunder Node: 77 numbers, 175 opened records, 175 saved drafts and 34 JSON literals, including every case in the review. Node'sJSON.stringifyoutput is read withjson.loads, and both sides are compared asjson.dumps(sort_keys=True). A table of 29 known JavaScript answers also runs without Node. The dry-run test checks thatexpected_countgiven as"12"or12.0passes and projects astype(...) is int._strict_jsonaddsparse_floatandparse_inthooks toparse_constant, in bothparse_assist_bodyandparse_model_output._workflow_assist_responsewritesjson.dumps(payload, allow_nan=False)intocurrent_app.response_class(..., mimetype='application/json'). OnTypeError,ValueErrororRecursionError, it logs one content-free error and returns 500assistant_failed. Tests cover each case. In the body,NaN,Infinity,-Infinity,1e999,-1e999, a nested1.5E400, a 400-digit integer and one past the 4,300-digit limit return 400invalid_request. In a model reply,NaN,Infinity,-Infinity,1e999,-1.5E400and a 400-digit integer each get a correction round, then 502 if they repeat, and a reply corrected on the second try succeeds. A stubbed NaN,Infinityor-Infinityresult at the route returnsassistant_failedand echoes nothing.is_workflow_assistant_enabled_for_usercover every case in the review. Another test checks that the V2 bootstrap reportsfeatures.enable_workflow_ai_assistantas False whenallow_user_workflowsis off._json_problemon repliesfinished_logAssertionErrorwhenlen(records) != 1, so-Okeeps the check.Extras found along the way:
_finite_intrefuses an integer too large for a double, which JavaScript reads asInfinity. It also refuses one past Python's 4,300-digitint()limit, asinvalid_request._js_numberused to raiseOverflowErrorfor a radix string or integer past the double range. It now returnsInfinity, as JavaScript does.RecursionErrorwhile serializing is handled like the other serialization errors.Mutation check. Each mutant was applied alone, then restored byte for byte:
\din_JS_DECIMALjson_numberkeeps whole floatsexpected_countcasesjson.loadsinparse_model_outputjson.loadsinparse_assist_bodyis True; defaultTrue_json_probleminparse_model_outputallow_nan=Truein_workflow_assist_response_finite_floatwithout its finite check_finite_intwithout its overflow checkPhase 4 merge and File Sync port (at 3ebc60c)
Phase 4 (#1580) landed on V2 first, at 0.261.207, so this PR moved to 0.261.208 and merged V2 at
6cc22fc8d.b6934b37arenumbers every 0.261.206 in this PR to 0.261.208.git grep 0.261.206finds nothing.d4ff642d6merges V2 with-c rerere.enabled=false. Four files were resolved by hand:config.py:0.261.208release_notes.md: the v0.261.208 section sits above Phase 4's v0.261.207, which is unchangedtest_route_blueprint_policy_inventory.py: both sides' policy entries and tests are kept, with a header line for each phase's coverageapp_surface.yml: regenerated withbuild_docs_inventory.py(122 capabilities), not hand-mergedAgainst V2,
config.pydiffers only in the VERSION line, the release notes only add this PR's section, and the inventory test only adds this PR's lines and theVersion:bump.3095cac6fports Phase 4's personal File Sync save. Phase 4 madeworkflowForSavespreadworkflowFileSyncForSavefor every scope, so the editor now sends an editedfile_syncfor a personal workflow. The Node candidate-parity tests caught the gap before the port: for a personal draft with edited File Sync, the editor savedcontinue_mode: "changed"while the Python projection kept the stored"always".workflow_file_sync_configandworkflow_file_sync_for_save, applied before the alert fields as in the TypeScript.test_workflow_assist_candidate_parity.pygains 11 File Sync saves. Each runs through the port and through the real editor under Node, and each is pinned, so the port is still checked without Node.test_workflow_assist_field_parity.pypins the two File Sync mode lists.preservedWorkflowFieldLabelsandworkflowSettingsDraftErrors, have no Python mirror here, so nothing else needed porting.3ebc60cdbupdates the feature doc: the port is done, and File Sync operations remain a follow-up.Mutation check on the port. Each mutant was applied alone, then restored byte for byte. All ten were killed:
workflow_for_savescope_typetopersonalenableduse_changed_documentsstrip()for source IDsidfallback forsource_idThe user moved this PR out of draft. The two CodeQL review threads on the test fakes were answered and resolved; see the CodeQL section.
CodeQL
CodeQL's check on
69989558creported 15 new alerts. Seven are existing alerts in code this PR doesn't touch, which the check lists because each now has a path from this route; they need no code change. The other eight were in this PR's own files:508bfa4bbfixes six and keeps two. CodeQL's re-run on508bfa4bblists nine alerts, the seven path alerts and the two kept notes, and marks the six as fixed. Its run on3ebc60cdb, after the Phase 4 merge and the File Sync port, lists the same nine and nothing new.Path alerts to dismiss. None of the seven is new to the repo. All seven are still open on
paullizer-react-v2-uiat6cc22fc8d, and the four log-injection alerts are open onDevelopmenttoo. Dismissing an alert closes it on every branch, so dismissing these here also closes them there. That's still the right call, because each reason below holds for any path into the sink, not only this route's. Only the three high alerts (#2847, #3404 and #2849) fail the check; the log-injection alerts are warnings and the two kept notes are notices.functions_workflow_alerts.pyL157 and L183_NESTED_QUANTIFIER_PATTERNruns only after the 200-character cap (WORKFLOW_ALERT_REGEX_MAX_LENGTH) is checked. At that cap, the slowest of 3,056 inputs I timed, including the shapes CodeQL names, took about 0.7 ms.functions_workflow_alerts.pyL161functions_appinsights.pyL419, L515, L531 and L553log_event. L419 is reached only throughdebug_print, which emits only when debug logging is on. Both helpers replace\r,\nand\tand redact secrets in the message and in every string property at any depth, then log under a constant format string, so no request value can forge a log line. The events this route writes itself also carry only closed values: the session'suser_id, codes and stage names, counts, error types,definition_version(an int from 1 to 3) and asubmission_idthat must match^[A-Za-z0-9._:-]{1,128}$.Fixed in
508bfa4bb, with no behavior change:ASSIST_REPLY_MAX_LENGTHimport fromfunctions_workflow_assist.pyand the unusedWORKFLOW_OUTPUT_KINDSimport fromfunctions_workflow_assist_operations.py._checked_focusends with an explicitraise, so it can't fall through toNone.except: passblocks in the run's telemetry says why it swallows: raising there would replace the request's own result.test_workflow_assist_dry_run_parity.pyL309 took awritessnapshot and never checked it. Every other snapshot in that file is checked after its run, so I restored the missingassert harness.writes() == writesafter the first run rather than deleting the snapshot.Kept. "Non-standard exception raised in special method" at
test_workflow_assist_limits_runtime.pyL761 and L803. The two fake containers'__getattr__raiseAssertionErrorfor a write method they don't define, on purpose:hasattr()andgetattr(..., default)swallowAttributeError, so a probe for a write method would otherwise go unnoticed. Any other missing attribute still raisesAttributeError. Both review threads on them were answered and resolved.Validation
Runner. The pinned venv
C:\Users\paullizer\.copilot\session-state\aa1bf1e0-b10d-4312-8dbb-143e9e9f9966\files\venv\Scripts\python.exe(Flask 3.1.3, Werkzeug 3.1.6) withPYTHONPATH=application/single_app;functional_tests, one file per process.assert, never a returned boolean. Their helpers are infunctional_tests/test_support/workflow_assist.py.3ebc60cdb, which includes V2 at6cc22fc8d, both normally and withpython -Ounless a row says otherwise. V2'sfunctional_tests/conftest.pyreturn-value guard was active, and its own test,test_pytest_return_false_guard.py, passes (7).pytest (
python [-O] -u -m pytest <file> -q -p no:cacheprovider):-Otest_workflow_assist_request.py(validation, strict JSON, sizes, turn counts, time zones, group, foreign, stale and deleted bases)test_workflow_assist_operations_security.py(forbidden fields, approval, raw IDs, unknown handles, group, escalation; no raw IDs in captured model messages, withfocusset; fenced and bounded excerpts; non-finite, lone-surrogate and over-deep replies)test_workflow_assist_scenarios.py("Done when": 7 AM weekdays with alerts and the Run-as warning;#checklistshared and used by the review task;#incident-reportbound to one task; ambiguous → question; un-normalized candidate; correction round → 502; content-free telemetry)test_workflow_assist_dry_run_parity.py(the PythonworkflowForSaveprojection through Phase 2's dry run, and the integerexpected_count)test_workflow_assist_limits_runtime.py(in flight, window,Retry-After, refunds, fail closed, model params, provider errors, and no writes except the limiter document)test_workflow_assist_field_parity.py(Python lists and violation rules againstWorkflowAuthoringHistory.tsxand the editor, read as text, including the File Sync mode lists)test_workflow_assist_candidate_parity.py(24 candidates through 3a's realapplyAssistandworkflowForSave, plus the type-strict corpus and 11 File Sync saves; 8 of the 15 tests run under Node)route_tests/test_workflow_assist_policy.py(decorator order, each gate and status code,Retry-After, the gate helper, the V2 flag, strict serialization)route_tests/test_route_blueprint_policy_inventory.pyroute_tests/test_route_unauthenticated_policy_contract.pyroute_tests/test_route_policy_test_coverage.py-Oadds pytest's warning that assertions outside test modules aren't executed. Test-module asserts are rewritten by pytest and still run.Phase 4 overlap. These pass at this head, normally and with
-O:test_orchestration_workflow_proposals_admin.py: 12test_orchestration_workflow_proposal_routes.py: 60test_personal_workflow_file_sync_sources.py: 22test_group_workflow_file_sync_client_parity.py: 179Playwright
ui_tests/test_admin_orchestration_workflow_proposals.pypasses 7 of 7, normally only. It needed a rebuild of the local V2 bundle first, which is gitignored and was stale after the merge.__main__(python [-O] -u <file>):-Otest_docs_app_surface_coverage.pytest_docs_site_quality.pytest_docs_link_integrity.pytest_docs_release_notes_integrity.pytest_v2_admin_capability_placement.pytest_v2_admin_workflow_parity.pytest_v2_admin_settings_schema.pyNotes on the failures. Each fails the same way on V2 at
6cc22fc8d:test_sub_settings_are_gated_by_their_capability, caused by another field's list-valueddepends_on.test_defaults_match_their_field_typefails oncore-plugin-toggles.m365_trusted_download_hosts, a default[]on a string field fromad85fc3c9on the base. It passes under-Oonly because its check is a bareassertin a__main__script, which-Ostrips. Pre-existing and not in this PR.Other checks.
python scripts/build_docs_inventory.pyregeneratesapp_surface.ymlwith no diff.python scripts/generate_feature_pages.py --checkconfirms the new stub matches. Four unrelated stubs are stale on the base (chat-memory,chat-orchestration,content-screeningandrate-limit-response-message), and I didn't regenerate them.application/v2_uiis identical to V2 at6cc22fc8d, because this PR changes no V2 source.npm run typecheckandnpm run buildpass at this head. The TS logic test lives underfunctional_tests/.Guardrails.
run_v2_guardrails.py <worktree> 6cc22fc8d HEAD 3b, with the venv;6cc22fc8dis the current V2 merge-base. All six checks passed:application/)V2's
scripts/check_xss_sinks.py, run the wayxss-sink-check.ymlruns it (--base-sha 6cc22fc8d --head-sha HEADover the 12 changedapplication/**files), also passed.CI on
3ebc60cdb. Every check passes exceptCodeQL, which lists the same nine alerts described in the CodeQL section.Follow-ups for 3c
#picker offers documents only, because tags return 400tags_unsupported.time_zone, the browser's IANA zone, so "7 AM" means the author's 7 AM.{role, text}. Replay an assistant turn as its reply plus the changes that were applied. Never replay failed or cancelled turns.Retry-After.features.enable_workflow_ai_assistant. Hide it for group and read-only workflows.candidatetoapplyAssist(candidate, {turnId, label}), and usechanges[].target.focus_keyfor Jump to. Showwarningson the turn.reply,errorand the shared rate-limit message as plain text. The rate-limit message is the admin's Markdown.base: nulland a draft without anid.workflow_for_saveport already mirrors its saveflowUnsupportedReasoninto the PythonworkflowForSave, so a draft that already has a publication the editor can't save (for example apdfone, which the save accepts) getsdraft_has_errorsRefs #1548
Part of #1543