diff --git a/dialect/agentscript/src/tests/lint.test.ts b/dialect/agentscript/src/tests/lint.test.ts index a0811163..90081f0c 100644 --- a/dialect/agentscript/src/tests/lint.test.ts +++ b/dialect/agentscript/src/tests/lint.test.ts @@ -2200,6 +2200,79 @@ 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('@actions'); + expect(errors[0].message).toContain('@utils'); + expect(errors[0].message).toContain('Identifier'); + }); + + 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('@actions'); + expect(errors[0].message).toContain('StringLiteral'); + }); + + 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('@actions'); + expect(errors[0].message).toContain('Ellipsis'); + }); + + 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('@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 5df6ca5f..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, @@ -155,6 +178,31 @@ 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. 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 kind = (node as { __kind?: string }).__kind ?? 'unknown'; + const allowed = ctx + ? formatAllowedNamespaces(constraints.resolvedType, ctx) + : '@namespace.member'; + attachDiagnostic( + node, + lintDiagnostic( + range, + `'${fieldName}' must be a member of ${allowed}. 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