feat: CALL NANOFLOW + GRANT/REVOKE nanoflow access#8
feat: CALL NANOFLOW + GRANT/REVOKE nanoflow access#8retran wants to merge 1 commit intopr4-nanoflows-create-dropfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds nanoflow composition and security management to MDL/mxcli by introducing CALL NANOFLOW within flow bodies and GRANT/REVOKE EXECUTE ON NANOFLOW, plus the SDK/BSON plumbing and executor validation needed to round-trip these constructs.
Changes:
- Extend MDL grammar/AST/visitor to support
CALL NANOFLOWandGRANT/REVOKE EXECUTE ON NANOFLOW. - Add SDK types + BSON parser/writer support for
NanoflowCallActionandNanoflow.AllowedModuleRoles. - Update executor flow builder + validation to build/validate nanoflow calls and nanoflow access statements; add docs/skills.
Reviewed changes
Copilot reviewed 32 out of 43 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| sdk/mpr/writer_microflow_actions.go | Serialize Microflows$NanoflowCallAction + nested call/mappings to BSON. |
| sdk/mpr/writer_microflow.go | Serialize Nanoflow.AllowedModuleRoles in nanoflow BSON. |
| sdk/mpr/parser_nanoflow.go | Parse AllowedModuleRoles from nanoflow BSON. |
| sdk/mpr/parser_microflow_actions.go | Parse Microflows$NanoflowCallAction and nested structures. |
| sdk/mpr/parser_microflow.go | Register parser for Microflows$NanoflowCallAction. |
| sdk/microflows/microflows_actions.go | Add SDK model types: NanoflowCallAction, NanoflowCall, NanoflowCallParameterMapping. |
| sdk/microflows/microflows.go | Add AllowedModuleRoles to Nanoflow SDK struct. |
| mdl/visitor/visitor_security.go | Build AST for GRANT/REVOKE nanoflow access statements. |
| mdl/visitor/visitor_microflow_statements.go | Dispatch + annotation wiring for CallNanoflowStmt. |
| mdl/visitor/visitor_microflow_actions.go | Build CallNanoflowStmt from parser context. |
| mdl/grammar/parser/mdlparser_listener.go | Regenerated listener interface for new productions. |
| mdl/grammar/parser/mdlparser_base_listener.go | Regenerated base listener stubs for new productions. |
| mdl/grammar/MDLParser.g4 | Grammar additions: callNanoflowStatement, grantNanoflowAccessStatement, revokeNanoflowAccessStatement. |
| mdl/executor/validate_microflow.go | Treat CallNanoflowStmt like other call statements for variable/error-handling validation. |
| mdl/executor/validate.go | Track nanoflow definitions + validate call nanoflow references in flow bodies. |
| mdl/executor/stmt_summary.go | Add summaries for GRANT/REVOKE nanoflow access statements. |
| mdl/executor/registry_test.go | Register new statement types in registry completeness tests. |
| mdl/executor/register_stubs.go | Register executor handlers for GRANT/REVOKE nanoflow access. |
| mdl/executor/nanoflow_validation.go | Include CallNanoflowStmt in nanoflow error-handling traversal. |
| mdl/executor/hierarchy.go | Remove trailing whitespace (no functional change). |
| mdl/executor/cmd_security_write.go | Implement GRANT/REVOKE nanoflow execute access by updating allowed roles. |
| mdl/executor/cmd_microflows_builder_validate.go | Track output var typing + validate error handler body for CallNanoflowStmt. |
| mdl/executor/cmd_microflows_builder_graph.go | Flow builder dispatch to create nanoflow call action. |
| mdl/executor/cmd_microflows_builder_calls.go | Build NanoflowCallAction + mappings + output variable integration. |
| mdl/executor/cmd_microflows_builder_annotations.go | Expose annotations for CallNanoflowStmt. |
| mdl/executor/cmd_microflows_builder.go | Lookup nanoflow return type via backend for typing inference. |
| mdl/executor/cmd_diff_mdl.go | Render CallNanoflowStmt into MDL for diff output. |
| mdl/catalog/builder_microflows_test.go | Import order formatting (no functional change). |
| mdl/catalog/builder.go | Alignment/formatting change only. |
| mdl/ast/ast_security.go | Add AST nodes for GRANT/REVOKE nanoflow access. |
| mdl/ast/ast_microflow.go | Add AST node CallNanoflowStmt. |
| docs/11-proposals/PROPOSAL_nanoflow_support.md | Add comprehensive nanoflow support proposal doc. |
| cmd/mxcli/syntax/registry.go | Whitespace/alignment only (no behavior change). |
| cmd/mxcli/syntax/features_workflow.go | Whitespace/alignment only (no behavior change). |
| cmd/mxcli/syntax/features_security.go | Whitespace/alignment only (no behavior change). |
| cmd/mxcli/syntax/features_page.go | Whitespace/alignment only (no behavior change). |
| cmd/mxcli/syntax/features_misc.go | Whitespace/alignment only (no behavior change). |
| cmd/mxcli/syntax/features_microflow.go | Whitespace/alignment only (no behavior change). |
| cmd/mxcli/syntax/features_integration.go | Whitespace/alignment only (no behavior change). |
| cmd/mxcli/syntax/features_domain_model.go | Whitespace/alignment only (no behavior change). |
| .claude/skills/mendix/write-nanoflows.md | Add agent skill guidance for writing nanoflows, including CALL + security. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // addCallNanoflowAction creates a CALL NANOFLOW statement. | ||
| func (fb *flowBuilder) addCallNanoflowAction(s *ast.CallNanoflowStmt) model.ID { | ||
| nfQN := s.NanoflowName.Module + "." + s.NanoflowName.Name | ||
|
|
||
| // Build parameter mappings for NanoflowCall | ||
| var mappings []*microflows.NanoflowCallParameterMapping | ||
| for _, arg := range s.Arguments { | ||
| paramQN := nfQN + "." + arg.Name | ||
| mapping := µflows.NanoflowCallParameterMapping{ | ||
| BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, | ||
| Parameter: paramQN, | ||
| Argument: fb.exprToString(arg.Value), | ||
| } | ||
| mappings = append(mappings, mapping) | ||
| } | ||
|
|
||
| nfCall := µflows.NanoflowCall{ | ||
| BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, | ||
| Nanoflow: nfQN, | ||
| ParameterMappings: mappings, | ||
| } | ||
|
|
||
| action := µflows.NanoflowCallAction{ | ||
| BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, | ||
| ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling), | ||
| NanoflowCall: nfCall, | ||
| ResultVariableName: s.OutputVariable, | ||
| UseReturnVariable: s.OutputVariable != "", | ||
| } |
There was a problem hiding this comment.
addCallNanoflowAction emits a *microflows.NanoflowCallAction, but the MDL formatter used by DESCRIBE NANOFLOW (formatAction in cmd_microflows_format_action.go) currently has no case for NanoflowCallAction, so describing a nanoflow containing this statement will render as an unsupported/unknown action. Add formatting support for NanoflowCallAction (and its parameter mappings + optional result variable) so describe output round-trips the new syntax.
| | annotation* raiseErrorStatement SEMICOLON? | ||
| | annotation* logStatement SEMICOLON? | ||
| | annotation* callMicroflowStatement SEMICOLON? | ||
| | annotation* callNanoflowStatement SEMICOLON? | ||
| | annotation* callJavaActionStatement SEMICOLON? |
There was a problem hiding this comment.
microflowStatement is used by both microflow and nanoflow bodies (via microflowBody). Adding callNanoflowStatement here makes CALL NANOFLOW syntactically valid inside microflows as well. This conflicts with the PR’s own nanoflow skill/proposal text stating microflows can’t call nanoflows; either move this alternative to a nanoflow-only statement rule, or add explicit validation in microflow creation/validation to reject CallNanoflowStmt in microflows (and update docs if the intent is actually to allow it).
| // addCallNanoflowAction creates a CALL NANOFLOW statement. | ||
| func (fb *flowBuilder) addCallNanoflowAction(s *ast.CallNanoflowStmt) model.ID { | ||
| nfQN := s.NanoflowName.Module + "." + s.NanoflowName.Name | ||
|
|
||
| // Build parameter mappings for NanoflowCall | ||
| var mappings []*microflows.NanoflowCallParameterMapping | ||
| for _, arg := range s.Arguments { | ||
| paramQN := nfQN + "." + arg.Name | ||
| mapping := µflows.NanoflowCallParameterMapping{ | ||
| BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, | ||
| Parameter: paramQN, | ||
| Argument: fb.exprToString(arg.Value), | ||
| } | ||
| mappings = append(mappings, mapping) | ||
| } | ||
|
|
||
| nfCall := µflows.NanoflowCall{ | ||
| BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, | ||
| Nanoflow: nfQN, | ||
| ParameterMappings: mappings, | ||
| } | ||
|
|
||
| action := µflows.NanoflowCallAction{ | ||
| BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, | ||
| ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling), | ||
| NanoflowCall: nfCall, | ||
| ResultVariableName: s.OutputVariable, | ||
| UseReturnVariable: s.OutputVariable != "", | ||
| } | ||
|
|
||
| activityX := fb.posX | ||
| activity := µflows.ActionActivity{ | ||
| BaseActivity: microflows.BaseActivity{ | ||
| BaseMicroflowObject: microflows.BaseMicroflowObject{ | ||
| BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, | ||
| Position: model.Point{X: fb.posX, Y: fb.posY}, | ||
| Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, | ||
| }, | ||
| AutoGenerateCaption: true, | ||
| }, | ||
| Action: action, | ||
| } | ||
|
|
||
| fb.objects = append(fb.objects, activity) | ||
| fb.posX += fb.spacing | ||
|
|
||
| if s.OutputVariable != "" { | ||
| fb.registerResultVariableType(s.OutputVariable, fb.lookupNanoflowReturnType(nfQN)) | ||
| } |
There was a problem hiding this comment.
There are existing unit tests covering addCallMicroflowAction result typing/behavior, but no analogous coverage for addCallNanoflowAction (e.g., parameter mapping format, UseReturnVariable/ResultVariableName, and return-type inference via lookupNanoflowReturnType). Adding targeted tests will help prevent regressions in CALL NANOFLOW serialization and type tracking.
2dee3d2 to
54a9353
Compare
2356562 to
4588cda
Compare
82247b4 to
2183899
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 35 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| model.BaseElement | ||
| ErrorHandlingType ErrorHandlingType `json:"errorHandlingType,omitempty"` | ||
| NanoflowCall *NanoflowCall `json:"nanoflowCall,omitempty"` | ||
| ResultVariableName string `json:"resultVariableName,omitempty"` |
There was a problem hiding this comment.
NanoflowCallAction uses ResultVariableName/resultVariableName naming, but the repository’s generated Mendix metamodel represents Microflows$NanoflowCallAction with an OutputVariableName field (json:"outputVariableName"). If the BSON field is also OutputVariableName, the current SDK type + BSON reader/writer will silently drop result variable assignments. Align the SDK field name/tag (and corresponding BSON keys in parser/writer) with the metamodel for nanoflow calls.
| ResultVariableName string `json:"resultVariableName,omitempty"` | |
| OutputVariableName string `json:"outputVariableName,omitempty"` |
| doc := bson.D{ | ||
| {Key: "$ID", Value: idToBsonBinary(string(a.ID))}, | ||
| {Key: "$Type", Value: "Microflows$NanoflowCallAction"}, | ||
| {Key: "ErrorHandlingType", Value: stringOrDefault(string(a.ErrorHandlingType), "Rollback")}, | ||
| {Key: "ResultVariableName", Value: a.ResultVariableName}, | ||
| {Key: "UseReturnVariable", Value: a.UseReturnVariable}, | ||
| } |
There was a problem hiding this comment.
serializeMicroflowAction writes NanoflowCallAction using the BSON key ResultVariableName. The generated metamodel indicates Microflows$NanoflowCallAction uses OutputVariableName (and JSON outputVariableName), so this may emit invalid/ignored output-variable data in Studio Pro. Verify the correct storage field name for nanoflow call actions and update the writer (and parser/types) accordingly.
| func parseNanoflowCallAction(raw map[string]any) *microflows.NanoflowCallAction { | ||
| action := µflows.NanoflowCallAction{} | ||
| action.ID = model.ID(extractBsonID(raw["$ID"])) | ||
| action.ErrorHandlingType = microflows.ErrorHandlingType(extractString(raw["ErrorHandlingType"])) | ||
| action.ResultVariableName = extractString(raw["ResultVariableName"]) | ||
| action.UseReturnVariable = extractBool(raw["UseReturnVariable"], false) | ||
|
|
There was a problem hiding this comment.
parseNanoflowCallAction reads the result variable from raw["ResultVariableName"]. If Mendix stores nanoflow call output variables under OutputVariableName (per generated metamodel), this will parse as empty and break round-tripping. Consider checking both keys (similar to other parsers that handle storageName vs qualifiedName differences) or switching to the correct key consistently with the writer.
| // Parse AllowedModuleRoles | ||
| for _, r := range extractBsonArray(raw["AllowedModuleRoles"]) { | ||
| if roleID, ok := r.(string); ok { | ||
| nf.AllowedModuleRoles = append(nf.AllowedModuleRoles, model.ID(roleID)) | ||
| } | ||
| } |
There was a problem hiding this comment.
The loop variable r in for _, r := range extractBsonArray(...) shadows the method receiver r *Reader, which makes the code harder to read and easy to trip over during future edits. Renaming the loop variable (e.g., role/item) would avoid the shadowing without changing behavior.
4588cda to
bc2a53a
Compare
2183899 to
bcb19c0
Compare
bc2a53a to
dabcdfb
Compare
bcb19c0 to
dd4df09
Compare
Summary
CALL NANOFLOWstatement for calling nanoflows from within flow bodies (grammar, AST, visitor, flow builder, validation)GRANT/REVOKE EXECUTE ON NANOFLOWfor nanoflow security management (grammar, AST, visitor, executor)NanoflowCallAction,NanoflowCall,NanoflowCallParameterMappingSDK types with BSON parser/writerAllowedModuleRolestoNanoflowstruct with BSON round-trip supportvalidate.gowrite-nanoflows.mdagentic skill (embedded in binary)PROPOSAL_nanoflow_support.md)Why
Completes P1 nanoflow parity: without CALL NANOFLOW, nanoflows cannot invoke other nanoflows — breaking compositional flow design. Without GRANT/REVOKE, security roles cannot be managed via CLI. The agentic skill enables LLM agents to write correct nanoflow MDL without microflow-only action mistakes.
Testing
make build && make test && make lint-goall pass