fix(core): Hide get-json from Instance AI (#36146)

This commit is contained in:
Predrag Ristic
2026-08-19 16:22:04 +00:00
committed by GitHub
parent 686da48e63
commit dfe7c02f03
7 changed files with 36 additions and 7 deletions
@@ -213,12 +213,12 @@ describe('workflows tool', () => {
expect(context.workflowService.publish).not.toHaveBeenCalled();
});
it('should allow code inspection but reject raw update on orchestrator surface', () => {
it('should allow code inspection but reject raw workflow actions on orchestrator surface', () => {
const context = createMockContext();
const tool = createWorkflowsTool(context, 'orchestrator');
const schema = getInputSchema(tool);
expect(schema.safeParse({ action: 'get-json', workflowId: 'w1' }).success).toBe(true);
expect(schema.safeParse({ action: 'get-json', workflowId: 'w1' }).success).toBe(false);
expect(schema.safeParse({ action: 'get-as-code', workflowId: 'w1' }).success).toBe(true);
expect(
schema.safeParse({
@@ -227,6 +227,8 @@ describe('workflows tool', () => {
workflow: { name: 'WF', nodes: [], connections: {} },
}).success,
).toBe(false);
expect(getDescription(tool)).toContain('TypeScript SDK code');
expect(getDescription(tool)).not.toContain('WorkflowJSON');
});
});
@@ -645,6 +647,8 @@ describe('workflows tool', () => {
structure: '// generated code',
note: STRUCTURE_ONLY_NOTE,
});
expect(STRUCTURE_ONLY_NOTE).toContain('get-as-code');
expect(STRUCTURE_ONLY_NOTE).not.toContain('get-json');
const codegenInput = vi.mocked(generateWorkflowCode).mock.calls[0][0];
expect(codegenInput).toMatchObject({ name: 'Test WF' });
expect(JSON.stringify(codegenInput)).not.toContain('conditions');
+1 -1
View File
@@ -127,7 +127,7 @@ export function createAllTools(context: InstanceAiContext): InstanceAiToolRegist
/**
* Creates orchestrator domain tools. Skills run in the orchestrator now, so
* domain tools keep their workflow-building surface while hiding raw full
* WorkflowJSON update actions.
* WorkflowJSON read and update actions.
*/
export function createOrchestratorDomainTools(context: InstanceAiContext): InstanceAiToolRegistry {
const tools: Array<[string, BuiltTool]> = [
@@ -20,6 +20,9 @@ function makeContext(
domainContext.workflowService.get = vi
.fn()
.mockResolvedValue({ id: 'w1', name: 'Test', nodes: [], connections: {}, active: false });
domainContext.workflowService.getAsWorkflowJSON = vi
.fn()
.mockResolvedValue({ name: 'Eval setup', nodes: [], connections: {} });
domainContext.workflowService.updateFromWorkflowJSON = vi
.fn()
.mockResolvedValue({ id: 'w1', name: 'Test', versionId: 'v1', nodes: [], connections: {} });
@@ -79,6 +82,7 @@ describe('buildEvalSetupTools', () => {
expect(schema.safeParse({ action: 'get-json', workflowId: 'w1' }).success).toBe(true);
expect(schema.safeParse({ action: 'get', workflowId: 'w1' }).success).toBe(false);
expect(schema.safeParse({ action: 'get-as-code', workflowId: 'w1' }).success).toBe(false);
expect(
schema.safeParse({
action: 'update',
@@ -96,6 +100,19 @@ describe('buildEvalSetupTools', () => {
).toBe(false);
});
it('reads raw workflow JSON through the eval setup workflow tool', async () => {
const ctx = makeContext('require_approval');
const workflows = buildEvalSetupTools(ctx).get('workflows');
const result = await workflows?.handler!({ action: 'get-json', workflowId: 'w1' }, {} as never);
expect(ctx.domainContext!.workflowService.getAsWorkflowJSON).toHaveBeenCalledWith(
'w1',
undefined,
);
expect(result).toEqual({ name: 'Eval setup', nodes: [], connections: {} });
});
it('leaves the original workflows tool in place when admin has blocked updates', () => {
const ctx = makeContext('blocked');
const tools = buildEvalSetupTools(ctx);
@@ -383,7 +383,7 @@ function getSupportedWorkflowActionSchemas(
return {
list: listAction,
get: getAction,
'get-json': getJsonAction,
...(surface !== 'orchestrator' ? { 'get-json': getJsonAction } : {}),
'get-as-code': getAsCodeAction,
delete: deleteAction,
unarchive: unarchiveAction,
@@ -218,6 +218,14 @@ describe('createBuildWorkflowTool', () => {
expect(tool.description).toContain('workflow-builder');
expect(tool.description).toContain('data-table-manager');
expect(tool.description).toContain('load_skill');
expect(tool.description).toContain(
'Use TypeScript SDK .workflow.ts source for new and existing workflows.',
);
expect(tool.description).not.toContain('WorkflowJSON .json source for existing workflow edits');
expect(buildWorkflowInputSchema.shape.filePath.description).toContain(
'Workspace-relative path to the TypeScript SDK workflow source file',
);
expect(buildWorkflowInputSchema.shape.filePath.description).not.toContain('WorkflowJSON');
});
it('builds a new workflow from a workspace source file', async () => {
@@ -141,7 +141,7 @@ export const buildWorkflowInputSchema = z
'Pass a workspace-relative path like src/workflows/my-workflow.workflow.ts.',
})
.describe(
'Workspace-relative path to the workflow source file to build, e.g. src/workflows/my-workflow.workflow.ts. Supports TypeScript SDK files and WorkflowJSON .json files.',
'Workspace-relative path to the TypeScript SDK workflow source file to build, e.g. src/workflows/my-workflow.workflow.ts.',
),
sourceCode: z
.string()
@@ -351,7 +351,7 @@ export function createBuildWorkflowTool(context: InstanceAiContext) {
'Build and save a workflow from workflow source. ' +
'Load `workflow-builder` via `load_skill` before calling this tool. ' +
'When the workflow creates or writes Data Tables, also load `data-table-manager` first. ' +
'Use TypeScript SDK source for new workflows, or WorkflowJSON .json source for existing workflow edits. ' +
'Use TypeScript SDK .workflow.ts source for new and existing workflows. ' +
'Prefer writing the file with `workspace_write_file` / `workspace_str_replace_file` so `workflow-sdk validate` can run on it, then call this tool with filePath. ' +
'For a one-shot create/rewrite you may pass `sourceCode` instead (the tool writes filePath and builds).',
)
@@ -5,7 +5,7 @@ import { NodeConnectionTypes } from 'n8n-workflow';
import type { WorkflowNode } from '../../types';
export const STRUCTURE_ONLY_NOTE =
'Node parameters omitted to keep context small. Pass full: true to include them in one call, or use get-json / get-as-code (optionally with versionId) for parameter-level detail.';
'Node parameters omitted to keep context small. Pass full: true to include them in one call, or use get-as-code (optionally with versionId) for parameter-level detail.';
// Below this, summarizing saves too little to be worth a possible second full fetch.
export const PARAMETERS_INLINE_LIMIT_BYTES = 4096;