Skip to content

Commit 9575cd2

Browse files
Fix scriptflow execution and add CLI commands (#2157)
- Fix NetworkAccess bool/string parameter mismatch between Node.js and PowerShell - Fix JSON array parsing in scriptHost.ps1 - Document ConstrainedLanguage mode restrictions in reasoning prompts - Add @ScriptFlow list/run/delete/show commands
1 parent f455eb3 commit 9575cd2

File tree

7 files changed

+310
-17
lines changed

7 files changed

+310
-17
lines changed

ts/packages/agents/scriptflow/flows/listFiles.flow.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,24 @@
2020
"id": "execute",
2121
"type": "script",
2222
"language": "powershell",
23-
"body": "param([string]$Path = '.', [string]$Filter = '*')\nGet-ChildItem -Path $Path -Filter $Filter | Format-Table Name, Length, LastWriteTime -AutoSize",
23+
"body": "param([string]$Path = '.', [string]$Filter = '*')\nGet-ChildItem -Path $Path -Filter $Filter | ForEach-Object { \"{0,-50} {1,15:N0} bytes {2}\" -f $_.Name, $_.Length, $_.LastWriteTime }",
2424
"parameters": {
2525
"Path": "${path}",
2626
"Filter": "${filter}"
2727
},
2828
"sandbox": {
2929
"allowedCmdlets": [
3030
"Get-ChildItem",
31-
"Format-Table",
31+
"ForEach-Object",
3232
"Select-Object",
3333
"Sort-Object"
3434
],
35-
"allowedPaths": ["$env:USERPROFILE", "$PWD", "$env:TEMP"],
35+
"allowedPaths": [
36+
"$env:USERPROFILE",
37+
"$env:USERPROFILE\\Downloads",
38+
"$PWD",
39+
"$env:TEMP"
40+
],
3641
"allowedModules": ["Microsoft.PowerShell.Management"],
3742
"maxExecutionTime": 30,
3843
"networkAccess": false

ts/packages/agents/scriptflow/samples/listFiles.recipe.json

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
],
2222
"script": {
2323
"language": "powershell",
24-
"body": "param([string]$Path = '.', [string]$Filter = '*')\nGet-ChildItem -Path $Path -Filter $Filter | Format-Table Name, Length, LastWriteTime -AutoSize",
25-
"expectedOutputFormat": "table"
24+
"body": "param([string]$Path = '.', [string]$Filter = '*')\nGet-ChildItem -Path $Path -Filter $Filter | ForEach-Object { \"{0,-50} {1,15:N0} bytes {2}\" -f $_.Name, $_.Length, $_.LastWriteTime }",
25+
"expectedOutputFormat": "text"
2626
},
2727
"grammarPatterns": [
2828
{
@@ -49,11 +49,16 @@
4949
"sandbox": {
5050
"allowedCmdlets": [
5151
"Get-ChildItem",
52-
"Format-Table",
52+
"ForEach-Object",
5353
"Select-Object",
5454
"Sort-Object"
5555
],
56-
"allowedPaths": ["$env:USERPROFILE", "$PWD", "$env:TEMP"],
56+
"allowedPaths": [
57+
"$env:USERPROFILE",
58+
"$env:USERPROFILE\\Downloads",
59+
"$PWD",
60+
"$env:TEMP"
61+
],
5762
"allowedModules": ["Microsoft.PowerShell.Management"],
5863
"maxExecutionTime": 30,
5964
"networkAccess": false

ts/packages/agents/scriptflow/scripts/scriptHost.ps1

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ param(
1818

1919
[string]$AllowedModulesJson = '[]',
2020

21-
[bool]$NetworkAccess = $false,
21+
[string]$NetworkAccess = "false",
2222

2323
[int]$TimeoutSeconds = 30
2424
)
@@ -28,8 +28,19 @@ $ErrorActionPreference = 'Stop'
2828
try {
2929
$allowedCmdlets = $AllowedCmdletsJson | ConvertFrom-Json
3030
$params = $ParametersJson | ConvertFrom-Json
31-
$AllowedPaths = @($AllowedPathsJson | ConvertFrom-Json)
32-
$AllowedModules = @($AllowedModulesJson | ConvertFrom-Json)
31+
# Parse allowed paths - must handle array properly to avoid PowerShell array unwrapping issues
32+
$parsedPaths = $AllowedPathsJson | ConvertFrom-Json
33+
if ($parsedPaths -is [array]) {
34+
$AllowedPaths = $parsedPaths
35+
} else {
36+
$AllowedPaths = @($parsedPaths)
37+
}
38+
$parsedModules = $AllowedModulesJson | ConvertFrom-Json
39+
if ($parsedModules -is [array]) {
40+
$AllowedModules = $parsedModules
41+
} else {
42+
$AllowedModules = @($parsedModules)
43+
}
3344

3445
# Expand environment variable references in allowed paths
3546
# (e.g. "$env:USERPROFILE" → "C:\Users\name")
@@ -72,8 +83,11 @@ try {
7283
}
7384
}
7485

86+
# Convert NetworkAccess string to boolean (handles "true"/"false"/"1"/"0"/"$true"/"$false")
87+
$networkAccessBool = $NetworkAccess -match '^(true|1|\$true)$'
88+
7589
# Network access enforcement
76-
if (-not $NetworkAccess) {
90+
if (-not $networkAccessBool) {
7791
# Define network-capable cmdlets that require networkAccess=true
7892
$NetworkCmdlets = @(
7993
'Invoke-WebRequest',

ts/packages/agents/scriptflow/src/actionHandler.mts

Lines changed: 232 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from "@typeagent/agent-sdk/helpers/action";
1616
import {
1717
type CommandHandler,
18+
type CommandHandlerNoParams,
1819
type CommandHandlerTable,
1920
getCommandInterface,
2021
} from "@typeagent/agent-sdk/helpers/command";
@@ -610,6 +611,12 @@ class ImportScriptHandler implements CommandHandler {
610611
implicitQuotes: true,
611612
},
612613
},
614+
flags: {
615+
actionName: {
616+
description: "Override the generated action name",
617+
type: "string",
618+
},
619+
},
613620
} as const;
614621
public async run(
615622
context: ActionContext<ScriptFlowAgentContext>,
@@ -643,11 +650,16 @@ class ImportScriptHandler implements CommandHandler {
643650
}
644651

645652
const analyzer = new ScriptAnalyzer();
646-
const recipe = await analyzer.analyze(scriptContent, resolvedPath);
653+
const overrideName = params.flags.actionName;
654+
const recipe = await analyzer.analyze(
655+
scriptContent,
656+
resolvedPath,
657+
overrideName,
658+
);
647659

648660
if (store.hasFlow(recipe.actionName)) {
649661
throw new Error(
650-
`A flow named '${recipe.actionName}' already exists. Delete it first or use a different name.`,
662+
`A flow named '${recipe.actionName}' already exists. Delete it first or use --actionName to specify a different name.`,
651663
);
652664
}
653665

@@ -663,9 +675,227 @@ class ImportScriptHandler implements CommandHandler {
663675
}
664676
}
665677

678+
class ListHandler implements CommandHandlerNoParams {
679+
public readonly description = "List all registered script flows";
680+
public async run(context: ActionContext<ScriptFlowAgentContext>) {
681+
const store = _agentStore;
682+
if (!store) {
683+
throw new Error("Script flow store not available");
684+
}
685+
const entries = store.listFlows();
686+
if (entries.length === 0) {
687+
context.actionIO.setDisplay("No script flows registered.");
688+
return;
689+
}
690+
const lines = entries.map(
691+
(e) =>
692+
` ${e.actionName}: ${e.description} [usage: ${e.usageCount}]${e.source === "seed" ? " (sample)" : ""}`,
693+
);
694+
context.actionIO.setDisplay(
695+
`Script flows (${entries.length}):\n${lines.join("\n")}`,
696+
);
697+
}
698+
}
699+
700+
class RunHandler implements CommandHandler {
701+
public readonly description = "Execute a script flow by name";
702+
public readonly parameters = {
703+
args: {
704+
flowName: {
705+
description: "Name of the script flow to execute",
706+
},
707+
},
708+
flags: {
709+
flowParametersJson: {
710+
description:
711+
'JSON string of parameters, e.g. \'{"path":"C:\\\\Users"}\'',
712+
type: "string",
713+
},
714+
},
715+
} as const;
716+
public async run(
717+
context: ActionContext<ScriptFlowAgentContext>,
718+
params: ParsedCommandParams<typeof this.parameters>,
719+
) {
720+
const store = _agentStore;
721+
if (!store) {
722+
throw new Error("Script flow store not available");
723+
}
724+
725+
const flowName = params.args.flowName;
726+
if (!flowName) {
727+
throw new Error("Missing required argument: flowName");
728+
}
729+
730+
const flow = await store.getFlow(flowName);
731+
if (!flow) {
732+
throw new Error(
733+
`Unknown script flow '${flowName}'. Use '@scriptflow list' to see available flows.`,
734+
);
735+
}
736+
737+
const script = await store.getScript(flowName);
738+
if (!script) {
739+
throw new Error(`Script not found for flow: ${flowName}`);
740+
}
741+
742+
let flowParameters: Record<string, unknown> = {};
743+
if (params.flags.flowParametersJson) {
744+
try {
745+
flowParameters = JSON.parse(params.flags.flowParametersJson);
746+
} catch {
747+
throw new Error(
748+
`Invalid JSON in --flowParametersJson: ${params.flags.flowParametersJson}`,
749+
);
750+
}
751+
}
752+
753+
expandEnvVarsInParams(flowParameters, flow.parameters);
754+
const pathError = validatePathParameters(
755+
flowParameters,
756+
flow.parameters,
757+
);
758+
if (pathError) {
759+
throw new Error(pathError);
760+
}
761+
const validationError = validateParameterRules(
762+
flowParameters,
763+
flow.parameters,
764+
);
765+
if (validationError) {
766+
throw new Error(validationError);
767+
}
768+
769+
const result = await executeFlowScript(flow, script, flowParameters);
770+
if (result.error !== undefined) {
771+
throw new Error(String(result.error));
772+
}
773+
774+
await store.recordUsage(flowName);
775+
if ("displayContent" in result && result.displayContent) {
776+
const content = result.displayContent;
777+
const text =
778+
typeof content === "string"
779+
? content
780+
: "content" in content
781+
? content.content
782+
: String(content);
783+
context.actionIO.setDisplay(text);
784+
}
785+
}
786+
}
787+
788+
class DeleteHandler implements CommandHandler {
789+
public readonly description = "Delete a script flow by name";
790+
public readonly parameters = {
791+
args: {
792+
name: {
793+
description: "Name of the script flow to delete",
794+
},
795+
},
796+
} as const;
797+
public async run(
798+
context: ActionContext<ScriptFlowAgentContext>,
799+
params: ParsedCommandParams<typeof this.parameters>,
800+
) {
801+
const store = _agentStore;
802+
if (!store) {
803+
throw new Error("Script flow store not available");
804+
}
805+
806+
const name = params.args.name;
807+
if (!name) {
808+
throw new Error("Missing required argument: name");
809+
}
810+
811+
const deleted = await store.deleteFlow(name);
812+
if (!deleted) {
813+
throw new Error(`Script flow not found: ${name}`);
814+
}
815+
816+
await context.sessionContext.reloadAgentSchema();
817+
context.actionIO.setDisplay(`Deleted script flow: ${name}`);
818+
}
819+
}
820+
821+
class ShowHandler implements CommandHandler {
822+
public readonly description = "Show details of a script flow";
823+
public readonly parameters = {
824+
args: {
825+
flowName: {
826+
description: "Name of the script flow to show",
827+
},
828+
},
829+
} as const;
830+
public async run(
831+
context: ActionContext<ScriptFlowAgentContext>,
832+
params: ParsedCommandParams<typeof this.parameters>,
833+
) {
834+
const store = _agentStore;
835+
if (!store) {
836+
throw new Error("Script flow store not available");
837+
}
838+
839+
const flowName = params.args.flowName;
840+
if (!flowName) {
841+
throw new Error("Missing required argument: flowName");
842+
}
843+
844+
const flow = await store.getFlow(flowName);
845+
if (!flow) {
846+
throw new Error(
847+
`Unknown script flow '${flowName}'. Use '@scriptflow list' to see available flows.`,
848+
);
849+
}
850+
851+
const script = await store.getScript(flowName);
852+
const entries = store.listFlows();
853+
const entry = entries.find((e) => e.actionName === flowName);
854+
855+
const paramLines = flow.parameters.map(
856+
(p) =>
857+
` ${p.name} (${p.type}${p.required ? ", required" : ""}): ${p.description}${p.default !== undefined ? ` [default: ${p.default}]` : ""}`,
858+
);
859+
const grammarLines = flow.grammarPatterns.map(
860+
(g) => ` "${g.pattern}"${g.isAlias ? " (alias)" : ""}`,
861+
);
862+
const cmdletList = flow.sandbox.allowedCmdlets.join(", ");
863+
864+
const output = [
865+
`Flow: ${flow.actionName}`,
866+
`Description: ${flow.description}`,
867+
`Display Name: ${flow.displayName}`,
868+
`Source: ${flow.source?.type ?? "unknown"}`,
869+
`Usage Count: ${entry?.usageCount ?? 0}`,
870+
"",
871+
"Parameters:",
872+
paramLines.length > 0 ? paramLines.join("\n") : " (none)",
873+
"",
874+
"Grammar Patterns:",
875+
grammarLines.length > 0 ? grammarLines.join("\n") : " (none)",
876+
"",
877+
"Sandbox:",
878+
` Cmdlets: ${cmdletList || "(none)"}`,
879+
` Timeout: ${flow.sandbox.maxExecutionTime}s`,
880+
` Network: ${flow.sandbox.networkAccess ? "allowed" : "blocked"}`,
881+
"",
882+
"Script:",
883+
"```powershell",
884+
script ?? "(script not found)",
885+
"```",
886+
];
887+
888+
context.actionIO.setDisplay(output.join("\n"));
889+
}
890+
}
891+
666892
const handlers: CommandHandlerTable = {
667893
description: "ScriptFlow commands",
668894
commands: {
895+
list: new ListHandler(),
896+
run: new RunHandler(),
897+
delete: new DeleteHandler(),
898+
show: new ShowHandler(),
669899
import: new ImportScriptHandler(),
670900
},
671901
};

ts/packages/agents/scriptflow/src/execution/powershellRunner.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export async function executeScript(
5555
"-AllowedCmdletsJson",
5656
JSON.stringify(request.sandbox.allowedCmdlets),
5757
"-NetworkAccess",
58-
String(request.sandbox.networkAccess),
58+
request.sandbox.networkAccess ? "true" : "false",
5959
"-TimeoutSeconds",
6060
String(request.sandbox.maxExecutionTime),
6161
];

ts/packages/dispatcher/dispatcher/src/context/appAgentManager.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1238,10 +1238,11 @@ export class AppAgentManager implements ActionConfigProvider {
12381238
return;
12391239
}
12401240

1241-
// Unload cached schema files so they get reloaded from disk
1241+
// Unload cached schema files and semantic map entries so they get reloaded
12421242
for (const schemaName of this.actionConfigs.keys()) {
12431243
if (getAppAgentName(schemaName) === appAgentName) {
12441244
this.actionSchemaFileCache.unloadActionSchemaFile(schemaName);
1245+
this.actionSemanticMap?.removeActionSchemaFile(schemaName);
12451246
}
12461247
}
12471248

0 commit comments

Comments
 (0)