From 91ce154da553c33376b1ecf85c0e9ac0e4689abe Mon Sep 17 00:00:00 2001 From: "puneet.monga" Date: Thu, 4 Jun 2026 09:43:33 -0700 Subject: [PATCH 1/2] fix(lint): reject non-MemberExpression colinear under resolvedType A reasoning action whose colinear value is a bare identifier, string literal, ellipsis, or lone @namespace silently bypassed the `resolvedType('invocationTarget')` check because the validator gated on `node instanceof MemberExpression`. The compiler then fell back to `target = name`, producing a tool that reaches Preview / Agentforce runtime but resolves to no implementation when invoked. Extend the constraint validator so any non-MemberExpression colinear under a `resolvedType` constraint emits a `constraint-resolved-type` Error pointing the author at the expected `@namespace.member` shape. The fix participates in the existing `validatedRefs` side channel so `undefined-reference` does not double-report. Adds four colocated tests covering bare identifier, string literal, ellipsis, and lone `@utils` cases in the existing `resolvedType constraint` describe block. --- dialect/agentscript/src/tests/lint.test.ts | 69 +++++++++++++++++++ .../src/lint/constraint-validation.ts | 21 ++++++ 2 files changed, 90 insertions(+) diff --git a/dialect/agentscript/src/tests/lint.test.ts b/dialect/agentscript/src/tests/lint.test.ts index a0811163..9508c3ed 100644 --- a/dialect/agentscript/src/tests/lint.test.ts +++ b/dialect/agentscript/src/tests/lint.test.ts @@ -2200,6 +2200,75 @@ start_agent main: ); expect(errors).toHaveLength(0); }); + + it('rejects bare identifier as invocation target', () => { + const diagnostics = runLint(` +start_agent main: + description: "test" + reasoning: + instructions: -> + |do it + actions: + hello: world +`); + const errors = diagnostics.filter( + d => d.code === 'constraint-resolved-type' + ); + expect(errors).toHaveLength(1); + expect(errors[0].message).toContain('@namespace.member'); + expect(errors[0].message).toContain('invocation target'); + }); + + it('rejects string literal as invocation target', () => { + const diagnostics = runLint(` +start_agent main: + description: "test" + reasoning: + instructions: -> + |do it + actions: + hello: "world" +`); + const errors = diagnostics.filter( + d => d.code === 'constraint-resolved-type' + ); + expect(errors).toHaveLength(1); + expect(errors[0].message).toContain('@namespace.member'); + }); + + it('rejects ellipsis as invocation target', () => { + const diagnostics = runLint(` +start_agent main: + description: "test" + reasoning: + instructions: -> + |do it + actions: + hello: ... +`); + const errors = diagnostics.filter( + d => d.code === 'constraint-resolved-type' + ); + expect(errors).toHaveLength(1); + expect(errors[0].message).toContain('@namespace.member'); + }); + + it('rejects lone @utils as invocation target', () => { + const diagnostics = runLint(` +start_agent main: + description: "test" + reasoning: + instructions: -> + |do it + actions: + hello: @utils +`); + const errors = diagnostics.filter( + d => d.code === 'constraint-resolved-type' + ); + expect(errors).toHaveLength(1); + expect(errors[0].message).toContain('@namespace.member'); + }); }); // ============================================================================ diff --git a/packages/language/src/lint/constraint-validation.ts b/packages/language/src/lint/constraint-validation.ts index 5df6ca5f..8b2c8d1d 100644 --- a/packages/language/src/lint/constraint-validation.ts +++ b/packages/language/src/lint/constraint-validation.ts @@ -155,6 +155,27 @@ function validateConstraints( } } + // resolvedType also rejects non-MemberExpression colinear values (bare + // identifiers, string literals, ellipsis, lone @ns). These compile to a + // no-op tool at runtime because compile-tool.ts only rebinds target for + // @actions.X / @connected_subagent.X member expressions. Every legitimate + // invocation/transition target is qualified as @namespace.member. + if (constraints.resolvedType && !(node instanceof MemberExpression)) { + validatedRefs?.add(node); + const label = resolvedTypeLabel(constraints.resolvedType); + const kind = (node as { __kind?: string }).__kind ?? 'unknown'; + attachDiagnostic( + node, + lintDiagnostic( + range, + `'${fieldName}' must be an @namespace.member ${label} (e.g. @actions.X). Got ${kind}.`, + DiagnosticSeverity.Error, + 'constraint-resolved-type' + ) + ); + return; + } + // Validate allowedNamespaces for ReferenceValue (MemberExpression) fields. // Early return is safe: MemberExpression nodes won't match extractStaticValue() // (which only handles NumberValue/BooleanValue/StringLiteral), so no downstream From 1f836c317151fc91353379ddf9bd406eed6eeea0 Mon Sep 17 00:00:00 2001 From: "puneet.monga" Date: Thu, 4 Jun 2026 13:25:47 -0700 Subject: [PATCH 2/2] fix(lint): enumerate valid namespaces in resolvedType diagnostic Per review feedback, replace the abstract "@namespace.member" wording with the concrete list of namespaces the active dialect accepts. Pull from ctx.invocationTargetNamespaces, drop aliases, and include @utils when the dialect ships it. Output by dialect: - agentscript: @actions, @connected_subagent, @subagent, @utils - agentforce: @actions, @connected_subagent, @response_formats, @subagent, @topic, @utils - agentfabric: @actions --- dialect/agentscript/src/tests/lint.test.ts | 14 +++++--- .../src/lint/constraint-validation.ts | 35 ++++++++++++++++--- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/dialect/agentscript/src/tests/lint.test.ts b/dialect/agentscript/src/tests/lint.test.ts index 9508c3ed..90081f0c 100644 --- a/dialect/agentscript/src/tests/lint.test.ts +++ b/dialect/agentscript/src/tests/lint.test.ts @@ -2215,8 +2215,9 @@ start_agent main: d => d.code === 'constraint-resolved-type' ); expect(errors).toHaveLength(1); - expect(errors[0].message).toContain('@namespace.member'); - expect(errors[0].message).toContain('invocation target'); + expect(errors[0].message).toContain('@actions'); + expect(errors[0].message).toContain('@utils'); + expect(errors[0].message).toContain('Identifier'); }); it('rejects string literal as invocation target', () => { @@ -2233,7 +2234,8 @@ start_agent main: d => d.code === 'constraint-resolved-type' ); expect(errors).toHaveLength(1); - expect(errors[0].message).toContain('@namespace.member'); + expect(errors[0].message).toContain('@actions'); + expect(errors[0].message).toContain('StringLiteral'); }); it('rejects ellipsis as invocation target', () => { @@ -2250,7 +2252,8 @@ start_agent main: d => d.code === 'constraint-resolved-type' ); expect(errors).toHaveLength(1); - expect(errors[0].message).toContain('@namespace.member'); + expect(errors[0].message).toContain('@actions'); + expect(errors[0].message).toContain('Ellipsis'); }); it('rejects lone @utils as invocation target', () => { @@ -2267,7 +2270,8 @@ start_agent main: d => d.code === 'constraint-resolved-type' ); expect(errors).toHaveLength(1); - expect(errors[0].message).toContain('@namespace.member'); + expect(errors[0].message).toContain('@actions'); + expect(errors[0].message).toContain('AtIdentifier'); }); }); diff --git a/packages/language/src/lint/constraint-validation.ts b/packages/language/src/lint/constraint-validation.ts index 8b2c8d1d..c0d3c8c8 100644 --- a/packages/language/src/lint/constraint-validation.ts +++ b/packages/language/src/lint/constraint-validation.ts @@ -106,6 +106,29 @@ function resolvedTypeLabel(resolvedType: BlockCapability): string { return resolvedType; } +/** + * Format the list of namespaces that satisfy a resolvedType constraint for a + * diagnostic message. Pulls capability-bearing namespaces from the dialect + * ctx, drops aliases (e.g. start_agent → subagent), and includes @utils when + * the dialect declares it (the only tool-bearing global scope today). + */ +function formatAllowedNamespaces( + resolvedType: BlockCapability, + ctx: SchemaContext +): string { + const capability = resolveCapabilityNamespaces(resolvedType, ctx); + const aliasKeys = new Set(Object.keys(ctx.info.aliases)); + const names = new Set(); + if (capability) + for (const n of capability) if (!aliasKeys.has(n)) names.add(n); + if (ctx.globalScopes.has('utils')) names.add('utils'); + if (names.size === 0) return '@namespace.member'; + return [...names] + .sort() + .map(n => `@${n}`) + .join(', '); +} + /** Validate a field value against its constraint metadata, attaching diagnostics to the AST node. */ function validateConstraints( value: unknown, @@ -158,17 +181,21 @@ function validateConstraints( // resolvedType also rejects non-MemberExpression colinear values (bare // identifiers, string literals, ellipsis, lone @ns). These compile to a // no-op tool at runtime because compile-tool.ts only rebinds target for - // @actions.X / @connected_subagent.X member expressions. Every legitimate - // invocation/transition target is qualified as @namespace.member. + // @actions.X / @connected_subagent.X member expressions. Enumerate the + // valid namespaces from the dialect ctx so the message stays correct + // across dialects (agentscript / agentforce / agentfabric ship different + // sets). if (constraints.resolvedType && !(node instanceof MemberExpression)) { validatedRefs?.add(node); - const label = resolvedTypeLabel(constraints.resolvedType); const kind = (node as { __kind?: string }).__kind ?? 'unknown'; + const allowed = ctx + ? formatAllowedNamespaces(constraints.resolvedType, ctx) + : '@namespace.member'; attachDiagnostic( node, lintDiagnostic( range, - `'${fieldName}' must be an @namespace.member ${label} (e.g. @actions.X). Got ${kind}.`, + `'${fieldName}' must be a member of ${allowed}. Got ${kind}.`, DiagnosticSeverity.Error, 'constraint-resolved-type' )